feat: GPU-downscale + software H.264 encode pipeline (WIP)

Add SwEncState in avhw.rs: GPU pipeline using scale_vaapi to downscale
4K BGRA -> 2K NV12 on AMD iGPU, then software encode with libopenh264.

- import_dma_buf_to_vaapi: av_hwframe_map based DMA-BUF import
- SwEncState: GPU filter graph (scale_vaapi) + NV12->YUV420P + libopenh264
- state_portal.rs: integrated SwEncState, auto DRM device detection
- vaapi_import_bench.rs: CPU vs GPU pipeline benchmark
- sw_encode_bench.rs: software encode benchmark

Benchmark results: GPU pipeline ~91 FPS theoretical (10.95ms/frame)
vs CPU pipeline ~33 FPS (30.21ms/frame).

Known issue: only 1 frame encoded in production recording,
diagnostic STATS logging added to debug frame flow.
This commit is contained in:
dailz
2026-05-29 22:04:12 +08:00
parent 55abb5e56d
commit d80b34f44f
9 changed files with 2416 additions and 305 deletions

View File

@@ -1,6 +1,7 @@
use std::ffi::CString;
use std::mem;
use std::os::fd::AsRawFd;
use std::os::fd::{AsRawFd, RawFd};
use std::os::raw::c_void;
use std::path::Path;
use std::ptr;
@@ -129,71 +130,125 @@ impl Drop for AvHwFrameCtx {
/// Test whether `drm_device` can import the PipeWire DMA-BUF frame via VAAPI.
pub fn test_dma_buf_import(drm_device: &Path, frame: &PwDmaBufFrame) -> Result<()> {
let hw_dev = AvHwDevCtx::new_vaapi(drm_device)?;
let frames = AvHwFrameCtx::for_capture(
&hw_dev,
frame.width,
frame.height,
ff::format::Pixel::RGBZ,
)?;
let frames =
AvHwFrameCtx::for_capture(&hw_dev, frame.width, frame.height, ff::format::Pixel::BGRA)?;
// SAFETY: AVDRMFrameDescriptor is a C POD struct. Zero-initialization is the
// expected FFmpeg setup before filling the fields used below.
let mut desc: ffi::AVDRMFrameDescriptor = unsafe { mem::zeroed() };
desc.nb_objects = 1;
desc.objects[0].fd = frame.fd.as_raw_fd();
desc.objects[0].size = 0;
desc.objects[0].format_modifier = frame.modifier;
desc.nb_layers = 1;
desc.layers[0].format = frame.format;
desc.layers[0].nb_planes = 1;
desc.layers[0].planes[0].object_index = 0;
desc.layers[0].planes[0].offset = frame.offset as isize;
desc.layers[0].planes[0].pitch = frame.stride as isize;
let desc_box = Box::new(desc);
let mut raw_frame = ff::frame::Video::empty();
// SAFETY: raw_frame owns a valid AVFrame. data[0] is used by FFmpeg's
// DRM_PRIME frame convention to point at an AVDRMFrameDescriptor. The Box is
// recovered before every return path below.
// SAFETY: frames is a live VAAPI frames context; frame carries valid DMA-BUF metadata.
unsafe {
let raw_ptr = raw_frame.as_mut_ptr();
(*raw_ptr).data[0] = Box::into_raw(desc_box) as *mut u8;
(*raw_ptr).format = ffi::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32;
(*raw_ptr).width = frame.width as i32;
(*raw_ptr).height = frame.height as i32;
}
let mut hw_frame = ff::frame::Video::empty();
// SAFETY: frames is an initialized AVHWFramesContext and hw_frame is a valid
// writable AVFrame wrapper.
let ret = unsafe { ffi::av_hwframe_get_buffer(frames.as_ptr(), hw_frame.as_mut_ptr(), 0) };
if ret < 0 {
// SAFETY: data[0] still contains the Box pointer installed above.
unsafe {
let _ = Box::from_raw((*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor);
(*raw_frame.as_mut_ptr()).data[0] = ptr::null_mut();
}
bail!("av_hwframe_get_buffer failed: error {ret}");
}
// SAFETY: hw_frame is a valid VAAPI frame allocated from `frames`; raw_frame
// is a DRM_PRIME source frame whose descriptor describes `frame`'s DMA-BUF.
let ret = unsafe { ffi::av_hwframe_transfer_data(hw_frame.as_mut_ptr(), raw_frame.as_ptr(), 0) };
// SAFETY: data[0] still contains the Box pointer installed above. Recover it
// before checking the transfer result so all paths clean up the descriptor.
unsafe {
let _ = Box::from_raw((*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor);
(*raw_frame.as_mut_ptr()).data[0] = ptr::null_mut();
}
if ret < 0 {
bail!("av_hwframe_transfer_data failed: error {ret}");
}
import_dma_buf_to_vaapi(
frames.as_ptr(),
frame.fd.as_raw_fd(),
frame.width,
frame.height,
frame.format,
frame.modifier,
frame.stride,
frame.offset,
)
}?;
Ok(())
}
/// Import a DMA-BUF into a VAAPI hardware frame via zero-copy `av_hwframe_map`.
///
/// # Safety
/// - `frames_ctx` must point to an initialized AVHWCramesContext for VAAPI
/// - `raw_fd` must be a valid DMA-BUF file descriptor
pub unsafe fn import_dma_buf_to_vaapi(
frames_ctx: *mut ffi::AVBufferRef,
raw_fd: RawFd,
width: u32,
height: u32,
drm_format: u32,
modifier: u64,
stride: u32,
offset: u64,
) -> Result<ff::frame::Video> {
let duped_fd = libc::dup(raw_fd);
if duped_fd < 0 {
bail!("dup(fd) failed: {}", std::io::Error::last_os_error());
}
let mut desc: ffi::AVDRMFrameDescriptor = mem::zeroed();
desc.nb_objects = 1;
desc.objects[0].fd = duped_fd;
desc.objects[0].size = (height as usize) * (stride as usize);
desc.objects[0].format_modifier = modifier;
desc.nb_layers = 1;
desc.layers[0].format = drm_format;
desc.layers[0].nb_planes = 1;
desc.layers[0].planes[0].object_index = 0;
desc.layers[0].planes[0].offset = offset as isize;
desc.layers[0].planes[0].pitch = stride as isize;
let desc_box = Box::new(desc);
let desc_ptr = Box::into_raw(desc_box);
let buf_ref = ffi::av_buffer_create(
desc_ptr as *mut u8,
std::mem::size_of::<ffi::AVDRMFrameDescriptor>(),
Some(cleanup_drm_descriptor),
ptr::null_mut(),
0,
);
if buf_ref.is_null() {
let desc_box = Box::from_raw(desc_ptr);
libc::close(desc_box.objects[0].fd);
bail!("av_buffer_create returned null for DRM descriptor");
}
let mut src = ff::frame::Video::empty();
{
let sp = src.as_mut_ptr();
(*sp).format = ffi::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32;
(*sp).width = width as i32;
(*sp).height = height as i32;
(*sp).data[0] = (*buf_ref).data;
(*sp).buf[0] = buf_ref;
}
let mut dst = ff::frame::Video::empty();
unsafe {
let dp = dst.as_mut_ptr();
(*dp).format = ffi::AVPixelFormat::AV_PIX_FMT_VAAPI as i32;
(*dp).hw_frames_ctx = ffi::av_buffer_ref(frames_ctx);
if (*dp).hw_frames_ctx.is_null() {
bail!("av_buffer_ref(frames_ctx) returned null");
}
}
let ret = unsafe {
ffi::av_hwframe_map(
dst.as_mut_ptr(),
src.as_ptr(),
ffi::AV_HWFRAME_MAP_READ as i32,
)
};
if ret < 0 {
let err_str = av_err_to_string(ret);
bail!("av_hwframe_map failed: error {ret} ({err_str})");
}
Ok(dst)
}
unsafe extern "C" fn cleanup_drm_descriptor(_opaque: *mut c_void, data: *mut u8) {
let desc = data as *mut ffi::AVDRMFrameDescriptor;
if !desc.is_null() && (*desc).nb_objects > 0 && (*desc).objects[0].fd >= 0 {
libc::close((*desc).objects[0].fd);
}
let _ = Box::from_raw(data as *mut ffi::AVDRMFrameDescriptor);
}
fn av_err_to_string(err: i32) -> String {
let mut buf = vec![0u8; 128];
unsafe {
ffi::av_strerror(err, buf.as_mut_ptr() as *mut i8, buf.len());
}
String::from_utf8_lossy(&buf)
.trim_end_matches('\0')
.to_string()
}
// ---------------------------------------------------------------------------
// EncState
// ---------------------------------------------------------------------------
@@ -234,9 +289,8 @@ impl EncState {
None => AvHwDevCtx::new_vaapi(drm_device)?,
};
// 2. Frame context for capture (XRGB/RGBZ)
let frames_rgb =
AvHwFrameCtx::for_capture(&hw_device_ctx, width, height, ff::format::Pixel::RGBZ)?;
AvHwFrameCtx::for_capture(&hw_device_ctx, width, height, ff::format::Pixel::BGRA)?;
// 3. Filter graph — must be built BEFORE encoder config so we can derive
// hw_frames_ctx from the buffersink output (correct surface pool dimensions).
@@ -538,6 +592,245 @@ impl EncState {
}
}
// ---------------------------------------------------------------------------
// SwEncState - VAAPI GPU downscale + software H.264 encode
// ---------------------------------------------------------------------------
pub struct SwEncState {
hw_dev: AvHwDevCtx,
frames_rgb: AvHwFrameCtx,
filter_graph: ff::filter::Graph,
sws_ctx: *mut ffi::SwsContext,
enc_video: ff::codec::encoder::video::Video,
octx: ff::format::context::Output,
yuv_frame: *mut ffi::AVFrame,
starting_timestamp: Option<i64>,
frames_written: bool,
}
unsafe impl Send for SwEncState {}
impl SwEncState {
#[allow(clippy::too_many_arguments)]
pub fn new(
drm_device: &Path,
output_path: &Path,
width: u32,
height: u32,
enc_width: u32,
enc_height: u32,
fps: u32,
bitrate: u64,
gop_size: u32,
) -> Result<Self> {
tracing::info!(
"SwEncState::new: GPU downscale {width}x{height} BGRA -> {enc_width}x{enc_height} NV12, software H.264"
);
let hw_dev = AvHwDevCtx::new_vaapi(drm_device)?;
let frames_rgb =
AvHwFrameCtx::for_capture(&hw_dev, width, height, ff::format::Pixel::BGRA)?;
let filter_graph = build_swenc_filter_graph(
&hw_dev,
&frames_rgb,
width,
height,
enc_width,
enc_height,
fps,
)?;
let sws_ctx = create_nv12_to_yuv420p_sws(enc_width, enc_height)?;
let (enc_video, octx) =
create_software_h264_muxer(output_path, enc_width, enc_height, fps, bitrate, gop_size)?;
let yuv_frame = alloc_yuv420p_frame(enc_width, enc_height)?;
Ok(Self {
hw_dev,
frames_rgb,
filter_graph,
sws_ctx,
enc_video,
octx,
yuv_frame,
starting_timestamp: None,
frames_written: false,
})
}
pub fn frames_rgb(&self) -> &AvHwFrameCtx {
&self.frames_rgb
}
pub fn encode_frame(&mut self, hw_frame: &ff::frame::Video) -> Result<()> {
let mut filter_src_ctx = self.filter_graph.get("in").unwrap();
let mut filter_src = filter_src_ctx.source();
let mut filter_sink_ctx = self.filter_graph.get("out").unwrap();
let mut filter_sink = filter_sink_ctx.sink();
filter_src
.add(hw_frame)
.map_err(|e| anyhow::anyhow!("software pipeline filter source add failed: {e}"))?;
loop {
let mut filtered = ff::frame::Video::empty();
match filter_sink.frame(&mut filtered) {
Ok(()) => {
if filtered.pts().is_none() {
filtered.set_pts(hw_frame.pts());
}
self.encode_filtered_frame(&filtered)?;
}
Err(ff::Error::Other { errno }) if errno == ffi::EAGAIN => break,
Err(e) => bail!("software pipeline filter sink get frame failed: {e}"),
}
}
Ok(())
}
pub fn flush(&mut self) -> Result<()> {
let mut filter_src_ctx = self.filter_graph.get("in").unwrap();
let mut filter_src = filter_src_ctx.source();
let _ = filter_src.flush();
let mut filter_sink_ctx = self.filter_graph.get("out").unwrap();
let mut filter_sink = filter_sink_ctx.sink();
loop {
let mut filtered = ff::frame::Video::empty();
match filter_sink.frame(&mut filtered) {
Ok(()) => self.encode_filtered_frame(&filtered)?,
Err(_) => break,
}
}
// SAFETY: Sending a null frame flushes the encoder without transferring ownership.
unsafe {
let ret = ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), ptr::null());
if ret < 0 && ret != ffi::AVERROR_EOF {
bail!("software encoder flush send failed: error {ret}");
}
}
let start_ts = self.starting_timestamp.unwrap_or(0);
self.drain_encoder(start_ts)?;
if self.frames_written {
self.octx
.write_trailer()
.map_err(|e| anyhow::anyhow!("Failed to write trailer: {e}"))?;
}
Ok(())
}
fn encode_filtered_frame(&mut self, filtered: &ff::frame::Video) -> Result<()> {
let mut sw_nv12 = unsafe { ffi::av_frame_alloc() };
if sw_nv12.is_null() {
bail!("av_frame_alloc failed for NV12 transfer frame");
}
// SAFETY: sw_nv12 is an allocated destination frame; filtered is a valid VAAPI NV12
// surface produced by scale_vaapi at encoder dimensions.
let transfer_ret = unsafe { ffi::av_hwframe_transfer_data(sw_nv12, filtered.as_ptr(), 0) };
if transfer_ret < 0 {
// SAFETY: sw_nv12 was allocated above and has not been freed yet.
unsafe { ffi::av_frame_free(&mut sw_nv12) };
bail!(
"av_hwframe_transfer_data failed for GPU-downscaled frame: error {transfer_ret} ({})",
av_err_to_string(transfer_ret)
);
}
// SAFETY: yuv_frame is an owned reusable YUV420P frame at the same dimensions as sw_nv12;
// sws_ctx was created for NV12 -> YUV420P with no resize, so sws_scale only converts format.
unsafe {
let ret = ffi::av_frame_make_writable(self.yuv_frame);
if ret < 0 {
ffi::av_frame_free(&mut sw_nv12);
bail!("av_frame_make_writable failed: error {ret}");
}
ffi::sws_scale(
self.sws_ctx,
(*sw_nv12).data.as_ptr() as *const *const u8,
(*sw_nv12).linesize.as_ptr() as *const i32,
0,
(*sw_nv12).height,
(*self.yuv_frame).data.as_ptr() as *mut *mut u8,
(*self.yuv_frame).linesize.as_ptr() as *const i32,
);
ffi::av_frame_free(&mut sw_nv12);
}
let pts = filtered.pts().unwrap_or(0);
if self.starting_timestamp.is_none() {
self.starting_timestamp = Some(pts);
}
let start_ts = self.starting_timestamp.unwrap_or(0);
// SAFETY: yuv_frame is initialized, writable, and matches the opened encoder format.
unsafe {
(*self.yuv_frame).pts = pts;
let ret = ffi::avcodec_send_frame(self.enc_video.as_mut_ptr(), self.yuv_frame);
if ret < 0 {
bail!("avcodec_send_frame failed for software encoder: error {ret}");
}
}
self.drain_encoder(start_ts)
}
fn drain_encoder(&mut self, start_ts: i64) -> Result<()> {
loop {
let mut pkt = ff::Packet::empty();
// SAFETY: enc_video is an open encoder; pkt is writable packet storage.
let ret = unsafe {
ffi::avcodec_receive_packet(self.enc_video.as_mut_ptr(), pkt.as_mut_ptr())
};
if ret < 0 {
if ret == ffi::AVERROR(ffi::EAGAIN) || ret == ffi::AVERROR_EOF {
break;
}
bail!("avcodec_receive_packet failed: error {ret}");
}
let enc_tb = self.enc_video.time_base();
let stream_tb = unsafe {
let streams = (*self.octx.as_ptr()).streams;
let st = *streams.add(0);
ff::Rational::from((*st).time_base)
};
pkt.rescale_ts(enc_tb, stream_tb);
if let Some(pts) = pkt.pts() {
pkt.set_pts(Some(pts - start_ts));
}
if let Some(dts) = pkt.dts() {
pkt.set_dts(Some(dts - start_ts));
}
pkt.set_stream(0);
pkt.write_interleaved(&mut self.octx)
.map_err(|e| anyhow::anyhow!("Failed to write packet: {e}"))?;
self.frames_written = true;
}
Ok(())
}
}
impl Drop for SwEncState {
fn drop(&mut self) {
if !self.sws_ctx.is_null() {
// SAFETY: sws_ctx is owned by this state and was returned by sws_getContext.
unsafe { ffi::sws_freeContext(self.sws_ctx) };
self.sws_ctx = ptr::null_mut();
}
if !self.yuv_frame.is_null() {
// SAFETY: yuv_frame is owned by this state and was allocated by av_frame_alloc.
unsafe { ffi::av_frame_free(&mut self.yuv_frame) };
}
}
}
// ---------------------------------------------------------------------------
// Shared encoder creation (used by both wlr-screencopy and portal paths)
// ---------------------------------------------------------------------------
@@ -559,11 +852,9 @@ pub fn create_encoder(
gop_size: Option<u32>,
existing_hw_ctx: Option<AvHwDevCtx>,
) -> Result<EncState> {
let (enc_w, enc_h) =
transpose_if_transform_transposed(transform, width as i32, height as i32);
let actual_bitrate = bitrate.unwrap_or_else(|| {
2 * (width as u64) * (height as u64) * (fps as u64) / 100
});
let (enc_w, enc_h) = transpose_if_transform_transposed(transform, width as i32, height as i32);
let actual_bitrate =
bitrate.unwrap_or_else(|| 2 * (width as u64) * (height as u64) * (fps as u64) / 100);
let actual_gop_size = gop_size.unwrap_or(fps);
EncState::new(
drm_device,
@@ -580,6 +871,247 @@ pub fn create_encoder(
)
}
// ---------------------------------------------------------------------------
// Software-encode GPU-downscale helpers
// ---------------------------------------------------------------------------
#[allow(clippy::too_many_arguments)]
fn build_swenc_filter_graph(
hw_dev: &AvHwDevCtx,
frames_rgb: &AvHwFrameCtx,
width: u32,
height: u32,
enc_width: u32,
enc_height: u32,
fps: u32,
) -> Result<ff::filter::Graph> {
let mut graph = ff::filter::Graph::new();
let buffersrc =
ff::filter::find("buffer").ok_or_else(|| anyhow::anyhow!("filter 'buffer' not found"))?;
let buffersink = ff::filter::find("buffersink")
.ok_or_else(|| anyhow::anyhow!("filter 'buffersink' not found"))?;
let scale_vaapi = ff::filter::find("scale_vaapi")
.ok_or_else(|| anyhow::anyhow!("filter 'scale_vaapi' not found"))?;
// FFmpeg 8.0+ rejects VAAPI pix_fmt in buffer args before hw_frames_ctx is attached.
// Use a SW placeholder, then override format/hw_frames_ctx with av_buffersrc_parameters_set.
let args = format!(
"video_size={}x{}:pix_fmt=bgra:time_base=1/{fps}:pixel_aspect=1/1",
width, height,
);
let mut src_ctx = graph.add(&buffersrc, "in", &args)?;
let par = unsafe { ffi::av_buffersrc_parameters_alloc() };
if par.is_null() {
bail!("av_buffersrc_parameters_alloc returned null");
}
// SAFETY: par and src_ctx are valid; frames_rgb.ref_clone returns an owned hw_frames_ctx ref
// that buffersrc consumes on successful parameter set.
unsafe {
(*par).format = Into::<ffi::AVPixelFormat>::into(ff::format::Pixel::VAAPI) as i32;
(*par).width = width as i32;
(*par).height = height as i32;
(*par).time_base = ffi::AVRational {
num: 1,
den: fps as i32,
};
(*par).hw_frames_ctx = frames_rgb.ref_clone();
let ret = ffi::av_buffersrc_parameters_set(src_ctx.as_mut_ptr(), par);
ffi::av_free(par as *mut _);
if ret < 0 {
bail!("av_buffersrc_parameters_set failed: error {ret}");
}
}
let mut scale_ctx = graph.add(
&scale_vaapi,
"scale",
&format!("{enc_width}:{enc_height}:format=nv12"),
)?;
// SAFETY: scale_vaapi keeps a ref-counted device context while the graph is alive.
unsafe {
(*scale_ctx.as_mut_ptr()).hw_device_ctx = hw_dev.ref_clone();
}
let mut sink_ctx = graph.add(&buffersink, "out", "")?;
src_ctx.link(0, &mut scale_ctx, 0);
scale_ctx.link(0, &mut sink_ctx, 0);
graph
.validate()
.map_err(|e| anyhow::anyhow!("software GPU filter graph validation failed: {e}"))?;
Ok(graph)
}
fn create_nv12_to_yuv420p_sws(width: u32, height: u32) -> Result<*mut ffi::SwsContext> {
// SAFETY: sws_getContext creates an owned scaler context for same-size NV12 -> YUV420P.
let ctx = unsafe {
ffi::sws_getContext(
width as i32,
height as i32,
ffi::AVPixelFormat::AV_PIX_FMT_NV12,
width as i32,
height as i32,
ffi::AVPixelFormat::AV_PIX_FMT_YUV420P,
2,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
)
};
if ctx.is_null() {
bail!("Failed to create NV12 -> YUV420P sws_scale context");
}
Ok(ctx)
}
fn alloc_yuv420p_frame(width: u32, height: u32) -> Result<*mut ffi::AVFrame> {
// SAFETY: Allocate an AVFrame, configure format/dimensions, then allocate writable buffers.
unsafe {
let mut frame = ffi::av_frame_alloc();
if frame.is_null() {
bail!("av_frame_alloc failed");
}
(*frame).width = width as i32;
(*frame).height = height as i32;
(*frame).format = ffi::AVPixelFormat::AV_PIX_FMT_YUV420P as i32;
let ret = ffi::av_frame_get_buffer(frame, 0);
if ret < 0 {
ffi::av_frame_free(&mut frame);
bail!("av_frame_get_buffer failed: error {ret}");
}
Ok(frame)
}
}
fn create_software_h264_muxer(
output_path: &Path,
width: u32,
height: u32,
fps: u32,
bitrate: u64,
gop_size: u32,
) -> Result<(
ff::codec::encoder::video::Video,
ff::format::context::Output,
)> {
let output_cstr = CString::new(output_path.to_str().unwrap())?;
let codec = ff::encoder::find_by_name("libopenh264")
.or_else(|| ff::encoder::find_by_name("libx264"))
.ok_or_else(|| {
anyhow::anyhow!("No H.264 software encoder found (tried libopenh264, libx264)")
})?;
let codec_name = codec.name().to_string();
let mut enc = {
let ctx = ff::codec::Context::new_with_codec(codec);
ctx.encoder().video()?
};
enc.set_width(width);
enc.set_height(height);
enc.set_format(ff::format::Pixel::YUV420P);
enc.set_bit_rate(bitrate as usize);
enc.set_gop(gop_size);
enc.set_time_base(ff::Rational::new(1, fps as i32));
enc.set_max_b_frames(0);
// SAFETY: global headers are needed by MP4 and harmless for other common muxers.
unsafe {
(*enc.as_mut_ptr()).flags |= ffi::AV_CODEC_FLAG_GLOBAL_HEADER as i32;
}
if codec_name == "libx264" {
// SAFETY: priv_data belongs to the unopened encoder; strings live for each call.
unsafe {
let key = CString::new("preset").unwrap();
let val = CString::new("veryfast").unwrap();
ffi::av_opt_set((*enc.as_mut_ptr()).priv_data, key.as_ptr(), val.as_ptr(), 0);
let key = CString::new("tune").unwrap();
let val = CString::new("zerolatency").unwrap();
ffi::av_opt_set((*enc.as_mut_ptr()).priv_data, key.as_ptr(), val.as_ptr(), 0);
}
}
let opened = enc
.open()
.map_err(|e| anyhow::anyhow!("Failed to open {codec_name} encoder: {e}"))?;
let enc_video = opened.0;
let use_null = output_path
.to_str()
.map(|s| s.contains("null"))
.unwrap_or(false);
let fmt_name = if use_null {
CString::new("null").unwrap()
} else {
CString::new("").unwrap()
};
let fmt_name_ptr = if use_null {
fmt_name.as_ptr()
} else {
ptr::null()
};
let mut fmt_ctx_ptr: *mut ffi::AVFormatContext = ptr::null_mut();
// SAFETY: fmt_ctx_ptr is initialized by FFmpeg; C strings live across the call.
let ret = unsafe {
ffi::avformat_alloc_output_context2(
&mut fmt_ctx_ptr,
ptr::null_mut(),
fmt_name_ptr,
output_cstr.as_ptr(),
)
};
if ret < 0 || fmt_ctx_ptr.is_null() {
bail!("Failed to allocate output format context: error {ret}");
}
// SAFETY: fmt_ctx_ptr is valid; stream and codec parameters are owned by the format context.
let stream_ptr = unsafe { ffi::avformat_new_stream(fmt_ctx_ptr, ptr::null()) };
if stream_ptr.is_null() {
bail!("Failed to create output stream");
}
// SAFETY: stream_ptr and encoder context are valid; parameters are copied into stream.
let ret =
unsafe { ffi::avcodec_parameters_from_context((*stream_ptr).codecpar, enc_video.as_ptr()) };
if ret < 0 {
bail!("Failed to copy codec parameters to stream: error {ret}");
}
// SAFETY: stream_ptr is valid and writable during muxer setup.
unsafe {
(*stream_ptr).time_base = (*enc_video.as_ptr()).time_base;
}
// SAFETY: open an AVIO only for muxers that require files; null muxer advertises NOFILE.
unsafe {
if (*(*fmt_ctx_ptr).oformat).flags & ffi::AVFMT_NOFILE == 0 {
let ret = ffi::avio_open(
&mut (*fmt_ctx_ptr).pb,
output_cstr.as_ptr(),
ffi::AVIO_FLAG_WRITE,
);
if ret < 0 {
bail!(
"Failed to open output file '{}': error {ret}",
output_path.display()
);
}
}
}
// SAFETY: fmt_ctx_ptr is fully configured.
let ret = unsafe { ffi::avformat_write_header(fmt_ctx_ptr, ptr::null_mut()) };
if ret < 0 {
bail!("Failed to write output header: error {ret}");
}
// SAFETY: ownership of fmt_ctx_ptr transfers to ffmpeg-next Output wrapper.
let octx = unsafe { ff::format::context::Output::wrap(fmt_ctx_ptr) };
tracing::info!("Using software H.264 encoder: {codec_name}");
Ok((enc_video, octx))
}
// ---------------------------------------------------------------------------
// Filter graph (inline)
// ---------------------------------------------------------------------------
@@ -668,8 +1200,7 @@ fn build_filter_graph(
Transform::Flipped270 => "0",
Transform::Normal => unreachable!(),
};
let mut trans_ctx =
graph.add(&transpose, "transpose", &format!("dir={dir_val}"))?;
let mut trans_ctx = graph.add(&transpose, "transpose", &format!("dir={dir_val}"))?;
unsafe {
(*trans_ctx.as_mut_ptr()).hw_device_ctx = hw_dev.ref_clone();
}

View File

@@ -137,10 +137,7 @@ pub fn detect_backend(args: &Args) -> Result<CaptureBackend> {
}
other => {
// 未知后端名称,返回错误
anyhow::bail!(
"Unknown backend '{}'. Use 'screencopy' or 'portal'.",
other
);
anyhow::bail!("Unknown backend '{}'. Use 'screencopy' or 'portal'.", other);
}
};
}

545
src/bin/sw_encode_bench.rs Normal file
View File

@@ -0,0 +1,545 @@
// sw_encode_bench.rs — Software encoding pipeline benchmark for screen capture
//
// Benchmarks: Portal capture -> mmap DMA-BUF -> sws_scale BGR0->YUV420P -> libx264 encode
//
// Usage: cargo run --bin sw_encode_bench -- --output /tmp/bench_test.mp4
use std::ffi::CString;
use std::os::fd::AsRawFd;
use std::path::Path;
use std::ptr;
use std::time::Instant;
use anyhow::{bail, Result};
use clap::Parser;
use ffmpeg_next as ff;
use ffmpeg_next::ffi;
use ffmpeg_next::packet::Mut;
use wl_webrtc::args::Args;
use wl_webrtc::cap_portal::{CapPortal, PwCtrlEvent};
#[derive(Parser, Debug)]
#[command(
name = "sw_encode_bench",
about = "Software encoding pipeline benchmark"
)]
struct BenchArgs {
#[arg(short, long)]
output: String,
#[arg(long, default_value_t = 120)]
frames: u32,
#[arg(long, default_value_t = 2560)]
enc_width: u32,
#[arg(long, default_value_t = 1440)]
enc_height: u32,
}
#[derive(Default)]
struct FrameStats {
mmap_us: Vec<u64>,
scale_us: Vec<u64>,
encode_us: Vec<u64>,
total_us: Vec<u64>,
mmap_failures: u32,
}
impl FrameStats {
fn avg_ms(data: &[u64]) -> f64 {
if data.is_empty() {
return 0.0;
}
data.iter().sum::<u64>() as f64 / data.len() as f64 / 1000.0
}
}
fn pix_fmt(p: ff::format::Pixel) -> ffi::AVPixelFormat {
Into::<ffi::AVPixelFormat>::into(p)
}
fn receive_first_frame(cap: &CapPortal) -> Result<wl_webrtc::cap_portal::PwDmaBufFrame> {
loop {
if let Ok(ctrl) = cap.event_receiver().try_recv() {
match ctrl {
PwCtrlEvent::StreamEnded => bail!("PipeWire stream ended before first frame"),
PwCtrlEvent::Error(e) => bail!("PipeWire error: {e}"),
}
}
match cap
.frame_receiver()
.recv_timeout(std::time::Duration::from_secs(10))
{
Ok(frame) => return Ok(frame),
Err(crossbeam_channel::RecvTimeoutError::Timeout) => {
bail!("Timeout waiting for first frame (10s)");
}
Err(crossbeam_channel::RecvTimeoutError::Disconnected) => {
bail!("PipeWire frame channel disconnected");
}
}
}
}
fn main() -> Result<()> {
let bench_args = BenchArgs::parse();
println!("=== Software Encode Benchmark ===");
println!("Output: {}", bench_args.output);
println!("Target frames: {}", bench_args.frames);
println!(
"Encode resolution: {}x{}",
bench_args.enc_width, bench_args.enc_height
);
println!();
ff::init()?;
println!("[1/4] Requesting screen capture via XDG Portal...");
println!(" (Select a screen to share in the portal dialog)");
let portal_args = Args {
output: bench_args.output.clone(),
output_name: None,
fps: 60,
codec: "h264".to_string(),
hw_accel: "vaapi".to_string(),
drm_device: None,
bitrate: None,
gop_size: None,
verbose: false,
backend: Some("portal".to_string()),
port: 0,
};
let cap = CapPortal::new(&portal_args)?;
println!("[1/4] Portal connected, PipeWire stream active\n");
println!("[2/4] Waiting for first frame from PipeWire...");
let first_frame = receive_first_frame(&cap)?;
let src_width = first_frame.width;
let src_height = first_frame.height;
let src_stride = first_frame.stride;
let enc_width = bench_args.enc_width;
let enc_height = bench_args.enc_height;
println!(
"[2/4] First frame: {}x{}, stride={}, format=0x{:08X}",
src_width, src_height, src_stride, first_frame.format
);
println!(
" Capture: {}x{} Encode: {}x{}\n",
src_width, src_height, enc_width, enc_height
);
println!("[3/4] Testing mmap on DMA-BUF...");
let mmap_size = (src_stride as usize) * (src_height as usize);
let mmap_ptr = unsafe {
libc::mmap(
ptr::null_mut(),
mmap_size,
libc::PROT_READ,
libc::MAP_SHARED,
first_frame.fd.as_raw_fd(),
first_frame.offset as i64,
)
};
if mmap_ptr == libc::MAP_FAILED {
let errno = std::io::Error::last_os_error();
bail!(
"mmap on DMA-BUF fd FAILED — AMD driver may not support \
CPU read of screen capture DMA-BUF buffers.\n\
Error: {} (errno={})\n\
\n\
Workarounds:\n\
1. Use VAAPI hardware import (av_hwframe_map) instead of mmap\n\
2. Use wlroots compositor with wlr-screencopy (SHM-based)\n\
3. Use a virtual display or software renderer",
errno,
errno.raw_os_error().unwrap_or(-1)
);
}
println!(
"[3/4] mmap SUCCESS — CPU can read DMA-BUF ({:.1} MB)\n",
mmap_size as f64 / 1024.0 / 1024.0
);
unsafe {
libc::munmap(mmap_ptr, mmap_size);
}
drop(first_frame);
// Set up libx264 encoder via FFI (same pattern as avhw.rs)
println!("[4/4] Setting up libx264 encoder...");
let output_path = Path::new(&bench_args.output);
let output_cstr = CString::new(output_path.to_str().unwrap())?;
// Try libx264 first (best quality/speed), fall back to openh264
let codec = ff::encoder::find_by_name("libx264")
.or_else(|| ff::encoder::find_by_name("libopenh264"))
.ok_or_else(|| {
anyhow::anyhow!("No H.264 software encoder found (tried libx264, libopenh264)")
})?;
println!("[4/4] Using encoder: {}\n", codec.name());
let mut enc = {
let ctx = ff::codec::Context::new_with_codec(codec);
ctx.encoder().video()?
};
enc.set_width(enc_width);
enc.set_height(enc_height);
enc.set_format(ff::format::Pixel::YUV420P);
enc.set_time_base(ff::Rational::new(1, 60));
enc.set_max_b_frames(0);
enc.set_gop(60);
let codec_name = codec.name();
if codec_name == "libx264" {
unsafe {
let key = CString::new("preset").unwrap();
let val = CString::new("veryfast").unwrap();
ffi::av_opt_set((*enc.as_mut_ptr()).priv_data, key.as_ptr(), val.as_ptr(), 0);
let key = CString::new("tune").unwrap();
let val = CString::new("zerolatency").unwrap();
ffi::av_opt_set((*enc.as_mut_ptr()).priv_data, key.as_ptr(), val.as_ptr(), 0);
}
}
let opened = enc.open()?;
let mut enc_video = opened.0;
// Create output format context via FFI
let mut fmt_ctx_ptr: *mut ffi::AVFormatContext = ptr::null_mut();
let ret = unsafe {
ffi::avformat_alloc_output_context2(
&mut fmt_ctx_ptr,
ptr::null_mut(),
ptr::null(),
output_cstr.as_ptr(),
)
};
if ret < 0 || fmt_ctx_ptr.is_null() {
bail!("Failed to allocate output format context: error {ret}");
}
let stream_ptr = unsafe { ffi::avformat_new_stream(fmt_ctx_ptr, ptr::null()) };
if stream_ptr.is_null() {
bail!("Failed to create new stream");
}
let ret =
unsafe { ffi::avcodec_parameters_from_context((*stream_ptr).codecpar, enc_video.as_ptr()) };
if ret < 0 {
bail!("Failed to copy encoder parameters: error {ret}");
}
unsafe {
(*stream_ptr).time_base = (*enc_video.as_ptr()).time_base;
}
let ret = unsafe {
ffi::avio_open(
&mut (*fmt_ctx_ptr).pb,
output_cstr.as_ptr(),
ffi::AVIO_FLAG_WRITE,
)
};
if ret < 0 {
bail!(
"Failed to open output file '{}': error {ret}",
output_path.display()
);
}
let ret = unsafe { ffi::avformat_write_header(fmt_ctx_ptr, ptr::null_mut()) };
if ret < 0 {
bail!("Failed to write header: error {ret}");
}
let mut octx = unsafe { ff::format::context::Output::wrap(fmt_ctx_ptr) };
// Create sws_scale context: BGRZ (BGR0) -> YUV420P
let bgr0_fmt = pix_fmt(ff::format::Pixel::BGRZ);
let yuv420p_fmt = pix_fmt(ff::format::Pixel::YUV420P);
let sws_ctx = unsafe {
ffi::sws_getContext(
src_width as i32,
src_height as i32,
bgr0_fmt,
enc_width as i32,
enc_height as i32,
yuv420p_fmt,
2,
ptr::null_mut(),
ptr::null_mut(),
ptr::null_mut(),
)
};
if sws_ctx.is_null() {
bail!("Failed to create sws_scale context");
}
// Allocate reusable YUV frame
let mut yuv_frame = unsafe {
let mut f = ffi::av_frame_alloc();
if f.is_null() {
bail!("av_frame_alloc failed");
}
(*f).width = enc_width as i32;
(*f).height = enc_height as i32;
(*f).format = yuv420p_fmt as i32;
let ret = ffi::av_frame_get_buffer(f, 0);
if ret < 0 {
ffi::av_frame_free(&mut f);
bail!("av_frame_get_buffer failed: {ret}");
}
f
};
println!(
"[4/4] Encoder ready: {}, {}x{}\n",
codec_name, enc_width, enc_height
);
println!("=== Encoding {} frames ===\n", bench_args.frames);
let mut stats = FrameStats::default();
let total_start = Instant::now();
let mut frames_encoded: u32 = 0;
let mut pts: i64 = 0;
while frames_encoded < bench_args.frames {
if let Ok(ctrl) = cap.event_receiver().try_recv() {
match ctrl {
PwCtrlEvent::StreamEnded => {
eprintln!("PipeWire stream ended after {} frames", frames_encoded);
break;
}
PwCtrlEvent::Error(e) => {
eprintln!("PipeWire error after {} frames: {}", frames_encoded, e);
break;
}
}
}
let frame = match cap
.frame_receiver()
.recv_timeout(std::time::Duration::from_secs(5))
{
Ok(f) => f,
Err(_) => {
eprintln!("Frame timeout/disconnect after {} frames", frames_encoded);
break;
}
};
let frame_start = Instant::now();
let mmap_start = Instant::now();
let frame_size = (frame.stride as usize) * (frame.height as usize);
let mmap_ptr = unsafe {
libc::mmap(
ptr::null_mut(),
frame_size,
libc::PROT_READ,
libc::MAP_SHARED,
frame.fd.as_raw_fd(),
frame.offset as i64,
)
};
if mmap_ptr == libc::MAP_FAILED {
stats.mmap_failures += 1;
eprintln!("mmap failed on frame {}", frames_encoded);
drop(frame);
continue;
}
stats.mmap_us.push(mmap_start.elapsed().as_micros() as u64);
let scale_start = Instant::now();
let src_data = unsafe { std::slice::from_raw_parts(mmap_ptr as *const u8, frame_size) };
unsafe {
ffi::av_frame_make_writable(yuv_frame);
let src_ptr = src_data.as_ptr();
let src_linesize = frame.stride as i32;
ffi::sws_scale(
sws_ctx,
&src_ptr as *const *const u8,
&src_linesize as *const i32,
0,
frame.height as i32,
(*yuv_frame).data.as_ptr() as *mut *mut u8,
(*yuv_frame).linesize.as_ptr() as *mut i32,
);
}
stats
.scale_us
.push(scale_start.elapsed().as_micros() as u64);
unsafe {
libc::munmap(mmap_ptr, frame_size);
}
drop(frame);
let encode_start = Instant::now();
unsafe {
(*yuv_frame).pts = pts;
pts += 1;
let ret = ffi::avcodec_send_frame(enc_video.as_mut_ptr(), yuv_frame);
if ret < 0 {
eprintln!("avcodec_send_frame failed: {ret}");
continue;
}
}
drain_encoder(&mut enc_video, &mut octx)?;
stats
.encode_us
.push(encode_start.elapsed().as_micros() as u64);
stats
.total_us
.push(frame_start.elapsed().as_micros() as u64);
frames_encoded += 1;
if frames_encoded % 30 == 0 {
let fps = frames_encoded as f64 / total_start.elapsed().as_secs_f64();
println!(
" [{}/{}] {:.1} FPS",
frames_encoded, bench_args.frames, fps
);
}
}
let total_elapsed = total_start.elapsed();
println!("\nFlushing encoder...");
unsafe {
ffi::avcodec_send_frame(enc_video.as_mut_ptr(), ptr::null());
}
drain_encoder(&mut enc_video, &mut octx)?;
octx.write_trailer()
.map_err(|e| anyhow::anyhow!("Failed to write trailer: {e}"))?;
// Cleanup
unsafe {
ffi::av_frame_free(&mut yuv_frame as *mut _);
ffi::sws_freeContext(sws_ctx);
}
drop(cap);
// Print results
let mmap_count = stats.mmap_us.len() as u32;
let mmap_success_rate = if mmap_count + stats.mmap_failures > 0 {
mmap_count as f64 / (mmap_count + stats.mmap_failures) as f64 * 100.0
} else {
0.0
};
let total_fps = frames_encoded as f64 / total_elapsed.as_secs_f64();
let avg_total_ms = FrameStats::avg_ms(&stats.total_us);
let max_fps = if avg_total_ms > 0.0 {
1000.0 / avg_total_ms
} else {
0.0
};
println!();
println!("╔══════════════════════════════════════════════════════════════╗");
println!("║ Software Encode Benchmark Results ║");
println!("╚══════════════════════════════════════════════════════════════╝");
println!();
println!("Capture resolution: {}x{}", src_width, src_height);
println!("Encode resolution: {}x{}", enc_width, enc_height);
println!("Frames encoded: {}", frames_encoded);
println!("Total time: {:.2}s", total_elapsed.as_secs_f64());
println!();
println!("mmap (DMA-BUF -> CPU):");
println!(
" avg: {:.2} ms/frame",
FrameStats::avg_ms(&stats.mmap_us)
);
println!(
" success rate: {:.1}% ({}/{})",
mmap_success_rate,
mmap_count,
mmap_count + stats.mmap_failures
);
println!();
println!("scale (BGR0 -> YUV420P via sws_scale):");
println!(
" avg: {:.2} ms/frame",
FrameStats::avg_ms(&stats.scale_us)
);
println!();
println!("encode ({}):", codec_name);
println!(
" avg: {:.2} ms/frame",
FrameStats::avg_ms(&stats.encode_us)
);
println!();
println!("total pipeline:");
println!(" avg: {:.2} ms/frame", avg_total_ms);
println!(" achieved FPS: {:.1}", total_fps);
println!(" max theoretical: {:.1} FPS", max_fps);
println!();
if mmap_success_rate < 100.0 {
println!(
"WARNING: Some mmap operations failed ({}/{})",
stats.mmap_failures,
stats.mmap_failures + mmap_count
);
}
if total_fps < 30.0 {
println!(
"NOTE: Achieved FPS ({:.1}) is below 30 FPS target.",
total_fps
);
}
println!("Output written to: {}", bench_args.output);
Ok(())
}
fn drain_encoder(
enc_video: &mut ff::encoder::video::Video,
octx: &mut ff::format::context::Output,
) -> Result<()> {
loop {
let mut pkt = ff::Packet::empty();
let ret = unsafe { ffi::avcodec_receive_packet(enc_video.as_mut_ptr(), pkt.as_mut_ptr()) };
if ret < 0 {
if ret == ffi::AVERROR(ffi::EAGAIN) || ret == ffi::AVERROR_EOF {
break;
}
eprintln!("avcodec_receive_packet failed: {ret}");
break;
}
let enc_tb = enc_video.time_base();
let stream_tb = unsafe {
let streams = (*octx.as_ptr()).streams;
let st = *streams.add(0);
ff::Rational::from((*st).time_base)
};
pkt.rescale_ts(enc_tb, stream_tb);
pkt.set_stream(0);
pkt.write_interleaved(octx)
.map_err(|e| anyhow::anyhow!("write packet failed: {e}"))?;
}
Ok(())
}

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
use std::thread::{self, JoinHandle};
use anyhow::Result;
use crossbeam_channel::{Receiver, Sender, bounded};
use crossbeam_channel::{bounded, Receiver, Sender};
use tokio::runtime::Runtime;
use crate::args::Args;
@@ -105,9 +105,7 @@ impl CapPortal {
// 通过 Portal 获取 PipeWire 连接 fd 和节点 ID
// block_on 在此处同步等待异步 Portal 调用完成
let (pw_fd, node_id) = rt.block_on(async {
Self::setup_portal().await
})?;
let (pw_fd, node_id) = rt.block_on(async { Self::setup_portal().await })?;
let (frame_tx, frame_rx) = bounded(3);
let (event_tx, event_rx) = bounded(8);
@@ -181,9 +179,9 @@ impl CapPortal {
use ashpd::desktop::PersistMode;
// 创建 Screencast D-Bus 代理,与桌面环境的 Portal 服务通信
let proxy = Screencast::new().await.map_err(|e| {
anyhow::anyhow!("Failed to create Screencast proxy: {e}")
})?;
let proxy = Screencast::new()
.await
.map_err(|e| anyhow::anyhow!("Failed to create Screencast proxy: {e}"))?;
// 创建 ScreenCast 会话(每个会话对应一次屏幕录制请求)
let session = proxy
@@ -287,10 +285,10 @@ impl Drop for CapPortal {
fn pipewire_thread(ctx: PwThreadCtx) {
use pipewire as pw;
use pw::properties::properties;
use pw::spa::param::video::VideoInfoRaw;
use pw::stream::{StreamBox, StreamFlags};
use std::cell::Cell;
use std::rc::Rc;
use pw::spa::param::video::VideoInfoRaw;
// 初始化 PipeWire 进程全局库。
//
@@ -329,9 +327,7 @@ fn pipewire_thread(ctx: PwThreadCtx) {
let core = match context.connect_fd(pw_fd, None) {
Ok(c) => c,
Err(e) => {
let _ = event_tx.try_send(PwCtrlEvent::Error(format!(
"connect_fd failed: {e}"
)));
let _ = event_tx.try_send(PwCtrlEvent::Error(format!("connect_fd failed: {e}")));
return;
}
};
@@ -357,8 +353,7 @@ fn pipewire_thread(ctx: PwThreadCtx) {
}
};
let format_info: Rc<Cell<Option<(u32, u32, u32, u64)>>> =
Rc::new(Cell::new(None));
let format_info: Rc<Cell<Option<(u32, u32, u32, u64)>>> = Rc::new(Cell::new(None));
let event_tx_state = event_tx.clone();
let _listener = stream
@@ -366,8 +361,7 @@ fn pipewire_thread(ctx: PwThreadCtx) {
.state_changed(move |_, _, old, new| {
tracing::debug!("PipeWire stream state: {old:?} -> {new:?}");
match new {
pw::stream::StreamState::Error(_)
| pw::stream::StreamState::Unconnected => {
pw::stream::StreamState::Error(_) | pw::stream::StreamState::Unconnected => {
let _ = event_tx_state.try_send(PwCtrlEvent::StreamEnded);
}
_ => {}
@@ -436,7 +430,8 @@ fn pipewire_thread(ctx: PwThreadCtx) {
// 从第一个数据项中获取 DMA-BUF 文件描述符
// 通过 libspa 的 Data 包装类型安全地访问 SPA 数据结构
let data_ref: &pw::spa::buffer::Data = unsafe { &*(datas_ptr as *const pw::spa::buffer::Data) };
let data_ref: &pw::spa::buffer::Data =
unsafe { &*(datas_ptr as *const pw::spa::buffer::Data) };
let fd = data_ref.fd();
if fd < 0 {
unsafe { stream.queue_raw_buffer(raw_buf) };
@@ -462,7 +457,8 @@ fn pipewire_thread(ctx: PwThreadCtx) {
for i in 0..n_metas {
let meta = &*metas.add(i as usize);
if meta.type_ == libspa::sys::SPA_META_Header
&& meta.size as usize >= std::mem::size_of::<libspa::sys::spa_meta_header>()
&& meta.size as usize
>= std::mem::size_of::<libspa::sys::spa_meta_header>()
&& !meta.data.is_null()
{
let header = &*(meta.data as *const libspa::sys::spa_meta_header);
@@ -505,9 +501,7 @@ fn pipewire_thread(ctx: PwThreadCtx) {
pts,
};
if let Err(crossbeam_channel::TrySendError::Full(_)) =
frame_tx.try_send(frame)
{
if let Err(crossbeam_channel::TrySendError::Full(_)) = frame_tx.try_send(frame) {
let prev = dropped.fetch_add(1, Ordering::Relaxed);
if prev > 0 && prev % 30 == 0 {
tracing::warn!("dropped {prev} frames total: encoder backlog");
@@ -593,35 +587,65 @@ const fn fourcc(a: u8, b: u8, c: u8, d: u8) -> u32 {
/// 此函数建立了两者之间的映射关系。
///
/// 支持的格式:
/// - BGRA/BGRx: 蓝绿红(Alpha/X) 32位格式
/// - RGBA/RGBx: 红绿蓝(Alpha/X) 32位格式
/// - ARGB/xRGB: Alpha/X-红绿蓝 32位格式 (映射为 AR24/XR24)
/// - ABGR/xBGR: Alpha/X-蓝绿红 32位格式 (映射为 AB24/XB24)
///
/// 不支持的格式返回 0
/// DRM 格式名描述像素值位布局(大端序),而非内存字节序。
/// 例如 DRM_FORMAT_ARGB8888 在小端 x86 上内存为 [B,G,R,A] = PipeWire BGRA。
fn spa_to_drm_fourcc(format: libspa::param::video::VideoFormat) -> u32 {
use drm_fourcc::DrmFourcc;
use libspa::param::video::VideoFormat;
match format {
VideoFormat::BGRA => fourcc(b'B', b'G', b'R', b'A'),
VideoFormat::BGRx => fourcc(b'B', b'G', b'R', b'X'),
VideoFormat::RGBA => fourcc(b'R', b'G', b'B', b'A'),
VideoFormat::RGBx => fourcc(b'R', b'G', b'B', b'X'),
VideoFormat::ARGB => fourcc(b'A', b'R', b'2', b'4'),
VideoFormat::xRGB => fourcc(b'X', b'R', b'2', b'4'),
VideoFormat::ABGR => fourcc(b'A', b'B', b'2', b'4'),
VideoFormat::xBGR => fourcc(b'X', b'B', b'2', b'4'),
// 不支持的格式返回 0调用者应检查此值
_ => 0, }
VideoFormat::BGRA => DrmFourcc::Argb8888 as u32,
VideoFormat::BGRx => DrmFourcc::Xrgb8888 as u32,
VideoFormat::RGBA => DrmFourcc::Abgr8888 as u32,
VideoFormat::RGBx => DrmFourcc::Xbgr8888 as u32,
VideoFormat::ARGB => DrmFourcc::Bgra8888 as u32,
VideoFormat::xRGB => DrmFourcc::Bgrx8888 as u32,
VideoFormat::ABGR => DrmFourcc::Rgba8888 as u32,
VideoFormat::xBGR => DrmFourcc::Rgbx8888 as u32,
_ => 0,
}
}
#[cfg(test)]
mod tests {
use super::*;
use drm_fourcc::DrmFourcc;
#[test]
fn spa_to_drm_fourcc_bgra() {
fn spa_to_drm_fourcc_all_32bit() {
use libspa::param::video::VideoFormat;
assert_eq!(spa_to_drm_fourcc(VideoFormat::BGRA), fourcc(b'B', b'G', b'R', b'A'));
assert_eq!(
spa_to_drm_fourcc(VideoFormat::BGRA),
DrmFourcc::Argb8888 as u32
);
assert_eq!(
spa_to_drm_fourcc(VideoFormat::BGRx),
DrmFourcc::Xrgb8888 as u32
);
assert_eq!(
spa_to_drm_fourcc(VideoFormat::RGBA),
DrmFourcc::Abgr8888 as u32
);
assert_eq!(
spa_to_drm_fourcc(VideoFormat::RGBx),
DrmFourcc::Xbgr8888 as u32
);
assert_eq!(
spa_to_drm_fourcc(VideoFormat::ARGB),
DrmFourcc::Bgra8888 as u32
);
assert_eq!(
spa_to_drm_fourcc(VideoFormat::xRGB),
DrmFourcc::Bgrx8888 as u32
);
assert_eq!(
spa_to_drm_fourcc(VideoFormat::ABGR),
DrmFourcc::Rgba8888 as u32
);
assert_eq!(
spa_to_drm_fourcc(VideoFormat::xBGR),
DrmFourcc::Rgbx8888 as u32
);
}
#[test]
@@ -629,10 +653,4 @@ mod tests {
use libspa::param::video::VideoFormat;
assert_eq!(spa_to_drm_fourcc(VideoFormat::NV12), 0);
}
#[test]
fn fourcc_values() {
assert_eq!(fourcc(b'B', b'G', b'R', b'A'), 0x41524742);
assert_eq!(fourcc(b'R', b'G', b'B', b'A'), 0x41424752);
}
}

9
src/lib.rs Normal file
View File

@@ -0,0 +1,9 @@
pub mod args;
pub mod avhw;
pub mod backend_detect;
pub mod cap_portal;
pub mod cap_wlr_screencopy;
pub mod fps_limit;
pub mod state;
pub mod state_portal;
pub mod transform;

View File

@@ -9,15 +9,15 @@ use wayland_client::globals::registry_queue_init;
use wayland_client::Connection;
// 各功能模块声明
mod args; // 命令行参数解析
mod avhw; // 音视频硬件加速
mod backend_detect; // 截屏后端自动检测wlroots vs Portal/PipeWire
mod cap_portal; // XDG Portal 屏幕捕获
mod cap_wlr_screencopy; // wlroots wlr-screencopy 截屏协议
mod fps_limit; // 帧率限制器
mod state; // wlr-screencopy 后端的主状态机
mod state_portal; // Portal/PipeWire 后端的主状态机
mod transform; // 图像变换(旋转/翻转)
mod args; // 命令行参数解析
mod avhw; // 音视频硬件加速
mod backend_detect; // 截屏后端自动检测wlroots vs Portal/PipeWire
mod cap_portal; // XDG Portal 屏幕捕获
mod cap_wlr_screencopy; // wlroots wlr-screencopy 截屏协议
mod fps_limit; // 帧率限制器
mod state; // wlr-screencopy 后端的主状态机
mod state_portal; // Portal/PipeWire 后端的主状态机
mod transform; // 图像变换(旋转/翻转)
use crate::args::Args;
use crate::cap_wlr_screencopy::CapWlrScreencopy;
@@ -65,12 +65,8 @@ fn main() -> Result<()> {
// 根据检测结果进入对应的事件循环
match backend {
crate::backend_detect::CaptureBackend::WlrScreencopy => {
run_wlr_screencopy(args)
}
crate::backend_detect::CaptureBackend::PortalPipeWire => {
run_portal_pipewire(args)
}
crate::backend_detect::CaptureBackend::WlrScreencopy => run_wlr_screencopy(args),
crate::backend_detect::CaptureBackend::PortalPipeWire => run_portal_pipewire(args),
}
}
@@ -130,7 +126,7 @@ fn run_wlr_screencopy(args: Args) -> Result<()> {
{
let mut pfd = libc::pollfd {
fd: wayland_fd,
events: libc::POLLIN, // 监听可读事件
events: libc::POLLIN, // 监听可读事件
revents: 0,
};
// timeout=0 表示非阻塞,立即返回当前 fd 状态
@@ -160,8 +156,8 @@ fn run_wlr_screencopy(args: Args) -> Result<()> {
// signal_hook_mio 将 Unix 信号转换为 fd 可读事件,
// 这样信号也可以通过 epoll 统一监听,不需要单独的信号处理器
let mut signals = signal_hook_mio::v1_0::Signals::new(&[
signal_hook::consts::SIGINT, // Ctrl+C
signal_hook::consts::SIGTERM, // kill 命令默认信号
signal_hook::consts::SIGINT, // Ctrl+C
signal_hook::consts::SIGTERM, // kill 命令默认信号
])?;
poll.registry()
.register(&mut signals, TOKEN_QUIT, Interest::READABLE)?;
@@ -305,11 +301,8 @@ fn run_portal_pipewire(args: Args) -> Result<()> {
// 只注册信号 fd没有 Wayland fd
// 所以 poll.poll 在这里只负责检测 SIGINT/SIGTERM
// 实际的帧采集完全依赖 poll_and_encode 的轮询
poll.registry().register(
&mut signals,
mio::Token(1),
mio::Interest::READABLE,
)?;
poll.registry()
.register(&mut signals, mio::Token(1), mio::Interest::READABLE)?;
// 主事件循环(超时 10ms比 wlr-screencopy 更短,因为不依赖 Wayland fd 唤醒)
// 10ms 超时的作用是让循环高频转动,以便及时处理 PipeWire 投递的帧

View File

@@ -568,11 +568,7 @@ impl<S: CaptureSource> State<S> {
tracing::error!("compositor copy failed");
let taken = mem::replace(&mut self.in_flight_surface, InFlightSurface::None);
match taken {
InFlightSurface::CopyQueued {
buffer,
frame,
..
} => {
InFlightSurface::CopyQueued { buffer, frame, .. } => {
drop(buffer);
if let EncConstructionStage::Streaming { cap, .. } = &mut self.stage {
cap.on_done_with_frame(frame);
@@ -594,7 +590,14 @@ impl<S: CaptureSource> State<S> {
cap,
screencopy_manager,
dmabuf,
} => (output_info, output, hw_device_ctx, cap, screencopy_manager, dmabuf),
} => (
output_info,
output,
hw_device_ctx,
cap,
screencopy_manager,
dmabuf,
),
other => {
tracing::warn!("negotiate_format: not in EverythingButFmt stage");
self.stage = other;
@@ -604,9 +607,10 @@ impl<S: CaptureSource> State<S> {
let (output_info, output, hw_device_ctx, cap, screencopy_manager, dmabuf) = stage_data;
let drm_path = self.resolve_drm_path();
let fps = self.args.fps;
let bitrate = self.args.bitrate.unwrap_or_else(|| {
2 * (width as u64) * (height as u64) * (fps as u64) / 100
});
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),
@@ -1199,11 +1203,7 @@ impl<S: CaptureSource> Dispatch<ZwpLinuxBufferParamsV1, ()> for State<S> {
tracing::error!("DMA-BUF buffer creation failed");
let taken = mem::replace(&mut state.in_flight_surface, InFlightSurface::None);
match taken {
InFlightSurface::CopyQueued {
buffer,
frame,
..
} => {
InFlightSurface::CopyQueued { buffer, frame, .. } => {
drop(buffer);
if let EncConstructionStage::Streaming { cap, .. } = &mut state.stage {
cap.on_done_with_frame(frame);
@@ -1239,9 +1239,7 @@ impl Dispatch<ZwlrScreencopyFrameV1, ()> for State<CapWlrScreencopy> {
// types (buffer and/or linux_dmabuf) before buffer_done. We only
// support DMA-BUF, so just log and wait for linux_dmabuf / buffer_done.
ScreencopyFrameEvent::Buffer { .. } => {
tracing::debug!(
"Received SHM Buffer offer — only DMA-BUF capture is supported"
);
tracing::debug!("Received SHM Buffer offer — only DMA-BUF capture is supported");
}
ScreencopyFrameEvent::LinuxDmabuf {
format,

View File

@@ -1,17 +1,13 @@
// 采集门户状态模块 —— 通过 PipeWire/DMA-BUF 进行屏幕采集并编码
use std::mem;
use std::os::fd::AsRawFd;
use std::path::PathBuf;
use anyhow::{bail, Result};
use ffmpeg_next as ff;
use ffmpeg_next::ffi;
use crate::args::Args;
use crate::avhw::{self, EncState};
use crate::avhw::{self, SwEncState};
use crate::cap_portal::{CapPortal, PwCtrlEvent, PwDmaBufFrame};
use crate::fps_limit::FpsLimit;
use crate::transform::Transform;
/// 门户采集的阶段状态
/// - WaitingForFormat: 等待接收到第一帧 DMA-BUF 以确定视频格式参数
@@ -28,8 +24,8 @@ enum PortalStage {
pub struct StatePortal {
/// 当前采集阶段
stage: PortalStage,
/// 件编码器状态(第一帧到达后才初始化)
enc: Option<EncState>,
/// GPU 缩放 + 软件编码器状态(第一帧到达后才初始化)
enc: Option<SwEncState>,
/// 帧率限制器
fps_limit: FpsLimit<()>,
/// PipeWire 屏幕采集端点
@@ -44,6 +40,14 @@ pub struct StatePortal {
drm_device: Option<PathBuf>,
/// 第一帧的时间戳(纳秒),用于计算相对 PTS
first_pts_ns: Option<i64>,
/// Diagnostic: frames received from PipeWire channel
frames_received: u64,
/// Diagnostic: frames dropped by FPS limiter
frames_fps_dropped: u64,
/// Diagnostic: frames successfully encoded
frames_encoded: u64,
/// Diagnostic: last time we printed stats
last_stats_time: Option<std::time::Instant>,
}
impl StatePortal {
@@ -70,6 +74,10 @@ impl StatePortal {
first_frame: true,
drm_device,
first_pts_ns: None,
frames_received: 0,
frames_fps_dropped: 0,
frames_encoded: 0,
last_stats_time: None,
})
}
@@ -94,7 +102,11 @@ impl StatePortal {
}
let frame = match self.cap.frame_receiver().try_recv() {
Ok(frame) => frame,
Ok(frame) => {
self.frames_received += 1;
tracing::debug!("poll_and_encode: got frame #{} from channel", self.frames_received);
frame
}
Err(_) => return Ok(false),
};
@@ -110,20 +122,35 @@ impl StatePortal {
);
let drm_path = self.resolve_drm_device_for_frame(&frame)?;
let enc = avhw::create_encoder(
let (enc_width, enc_height) = portal_encode_dimensions(frame.width, frame.height);
tracing::info!(
"Portal software encode target: {}x{} -> {}x{} @ {} fps",
frame.width,
frame.height,
enc_width,
enc_height,
self.args.fps,
);
let actual_bitrate = self.args.bitrate.unwrap_or_else(|| {
2 * (enc_width as u64) * (enc_height as u64) * (self.args.fps as u64) / 100
});
let actual_gop_size = self.args.gop_size.unwrap_or(self.args.fps);
let enc = avhw::SwEncState::new(
&drm_path,
self.args.output.as_ref(),
frame.width,
frame.height,
enc_width,
enc_height,
self.args.fps,
Transform::Normal,
self.args.bitrate,
self.args.gop_size,
None,
actual_bitrate,
actual_gop_size,
)?;
self.enc = Some(enc);
self.stage = PortalStage::Streaming;
tracing::info!("First frame processed, encoder initialized, transitioning to Streaming");
drop(frame);
}
PortalStage::Streaming => {
@@ -149,160 +176,105 @@ impl StatePortal {
match crate::avhw::test_dma_buf_import(candidate, frame) {
Ok(()) => {
tracing::info!(
"Auto-selected DRM device: {} (can import PipeWire DMA-BUF)",
candidate.display()
"Auto-detected DRM device: {} (tested {} candidates)",
candidate.display(),
candidates.len(),
);
self.drm_device = Some(candidate.clone());
return Ok(candidate.clone());
}
Err(err) => {
Err(e) => {
tracing::debug!(
"DRM device {} cannot import frame: {err:#}",
candidate.display()
"DRM device {} cannot import DMA-BUF: {e}",
candidate.display(),
);
failures.push(format!("{}: {err:#}", candidate.display()));
failures.push((candidate, e));
}
}
}
bail!(
"No DRM render device can import the PipeWire DMA-BUF frame. \
Specify --drm-device. Tried: {}",
failures.join("; ")
)
"No DRM render device can import the DMA-BUF frame. Tried: {}",
failures
.into_iter()
.map(|(p, e)| format!("{} ({e})", p.display()))
.collect::<Vec<_>>()
.join(", ")
);
}
/// 处理单帧 DMA-BUF 数据
///
/// 完整的帧处理流水线
/// 1. 帧率限制(首帧跳过)
/// 2. 构建 DRM 描述符
/// 3. 分配 DRM_PRIME 源帧
/// 4. 分配 VAAPI 硬件目标帧
/// 5. 通过 DMA-BUF 导入将帧数据导入 VAAPI
/// 6. 计算 PTS 时间戳
/// 7. 回收 DRM 描述符内存
/// 8. 编码输出
/// 通过 `av_hwframe_map` 零拷贝导入 VAAPI然后交给 SwEncState 完成
/// scale_vaapi GPU 缩放、2K NV12 回读、YUV420P 格式转换、软件 H.264 编码。
fn handle_pw_frame(&mut self, frame: PwDmaBufFrame) -> Result<()> {
// 1. FPS limiting (first frame bypasses)
// 帧率限制(首帧跳过限制,确保立即编码)
if self.first_frame {
self.first_frame = false;
} else {
let now = std::time::Instant::now();
if self.fps_limit.on_new_frame((), now).is_none() {
self.frames_fps_dropped += 1;
tracing::debug!("handle_pw_frame: FPS limit, dropping frame (#{})", self.frames_fps_dropped);
self.maybe_print_stats(now);
return Ok(());
}
}
// 2. Build DRM descriptor for DMA-BUF import
// 根据 DMA-BUF 帧信息构建 FFmpeg DRM 描述符
let desc = build_drm_descriptor(&frame);
let desc_box = Box::new(desc);
tracing::debug!("handle_pw_frame: processing frame, pts={}", frame.pts);
// 3. Allocate raw DRM_PRIME source frame using Video wrapper
// 分配 DRM_PRIME 格式的源帧,将描述符指针挂载到 data[0]
let mut raw_frame = ff::frame::Video::empty();
unsafe {
let raw_ptr = raw_frame.as_mut_ptr();
(*raw_ptr).data[0] = Box::into_raw(desc_box) as *mut u8;
(*raw_ptr).format = ffi::AVPixelFormat::AV_PIX_FMT_DRM_PRIME as i32;
(*raw_ptr).width = frame.width as i32;
(*raw_ptr).height = frame.height as i32;
}
// 4. Get encoder reference
// 获取编码器引用
let enc = match self.enc.as_mut() {
Some(e) => e,
None => {
// Recover the Box to prevent memory leak of the descriptor
// 编码器未初始化时回收描述符以防止内存泄漏
unsafe {
let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor;
if !desc_ptr.is_null() {
let _ = Box::from_raw(desc_ptr);
}
(*raw_frame.as_mut_ptr()).data[0] = std::ptr::null_mut();
}
bail!("encoder not initialized");
}
Some(enc) => enc,
None => bail!("encoder not initialized"),
};
// 5. Allocate VAAPI hardware target frame
// 分配 VAAPI 硬件帧缓冲区
let mut hw_frame = ff::frame::Video::empty();
let ret = unsafe {
ffi::av_hwframe_get_buffer(enc.frames_rgb().as_ptr(), hw_frame.as_mut_ptr(), 0)
};
if ret < 0 {
// Recover the Box to prevent memory leak of the descriptor
// 分配失败时回收描述符防止内存泄漏
unsafe {
let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor;
if !desc_ptr.is_null() {
let _ = Box::from_raw(desc_ptr);
}
(*raw_frame.as_mut_ptr()).data[0] = std::ptr::null_mut();
}
bail!("av_hwframe_get_buffer failed: error {ret}");
}
// SAFETY: frames_rgb is a live VAAPI frames context configured for capture; frame carries
// valid DMA-BUF fd/format/modifier/stride/offset metadata for the duration of this call.
let mut vaapi_frame = unsafe {
avhw::import_dma_buf_to_vaapi(
enc.frames_rgb().as_ptr(),
frame.fd.as_raw_fd(),
frame.width,
frame.height,
frame.format,
frame.modifier,
frame.stride,
frame.offset,
)
}?;
// 6. Import DMA-BUF into VAAPI via transfer_data
// 通过 DMA-BUF 导入将帧数据从 DRM 传输到 VAAPI 硬件表面
let ret = unsafe {
ffi::av_hwframe_transfer_data(hw_frame.as_mut_ptr(), raw_frame.as_ptr(), 0)
};
if ret < 0 {
// 传输失败时回收描述符防止内存泄漏
unsafe {
let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor;
if !desc_ptr.is_null() {
let _ = Box::from_raw(desc_ptr);
}
(*raw_frame.as_mut_ptr()).data[0] = std::ptr::null_mut();
}
if ret == -(ffi::EINVAL as i32) {
bail!(
"VAAPI does not support DMA-BUF modifier 0x{:X}",
frame.modifier
);
}
bail!("av_hwframe_transfer_data failed: error {ret}");
}
tracing::debug!("handle_pw_frame: DMA-BUF import OK");
// 7. Set PTS — convert PipeWire nanoseconds to encoder frame-number units
let pts = compute_pts(&mut self.first_pts_ns, frame.pts, self.args.fps);
unsafe {
(*hw_frame.as_mut_ptr()).pts = pts;
(*vaapi_frame.as_mut_ptr()).pts = pts;
}
// 8. Recover the Boxed descriptor from raw_frame *before* encoding.
// av_hwframe_transfer_data has already imported the DMA-BUF into the
// VAAPI surface, so FFmpeg no longer references the descriptor struct.
// Doing this before encode_frame ensures the descriptor is reclaimed
// even if encode_frame returns early via `?`.
//
// 在编码前回收描述符内存。
// 此时 DMA-BUF 数据已导入 VAAPI 表面FFmpeg 不再引用描述符结构体。
// 在 encode_frame 之前回收确保即使编码返回错误也能正确释放内存。
unsafe {
let desc_ptr = (*raw_frame.as_ptr()).data[0] as *mut ffi::AVDRMFrameDescriptor;
if !desc_ptr.is_null() {
let _ = Box::from_raw(desc_ptr);
}
(*raw_frame.as_mut_ptr()).data[0] = std::ptr::null_mut();
}
enc.encode_frame(&vaapi_frame)?;
self.frames_encoded += 1;
tracing::info!("handle_pw_frame: frame #{} encoded OK, pts={}", self.frames_encoded, pts);
let now = std::time::Instant::now();
self.maybe_print_stats(now);
// 9. Encode — safe to early-return via `?` now that descriptor is recovered.
// 编码帧数据(此时描述符已回收,可安全通过 `?` 提前返回)
enc.encode_frame(&hw_frame)?;
// raw_frame and hw_frame drop here via Video::drop → av_frame_free
// raw_frame 和 hw_frame 在此处通过 Video::drop → av_frame_free 释放
Ok(())
}
fn maybe_print_stats(&mut self, now: std::time::Instant) {
let should_print = match self.last_stats_time {
None => true,
Some(last) => now.duration_since(last) >= std::time::Duration::from_secs(2),
};
if should_print {
self.last_stats_time = Some(now);
tracing::info!(
"STATS: received={}, fps_dropped={}, encoded={}",
self.frames_received,
self.frames_fps_dropped,
self.frames_encoded,
);
}
}
/// 关闭状态:刷新编码器并清理资源
///
/// 使用 `enc.take()` 确保编码器只被 flush 一次,即使多次调用也安全(幂等)。
@@ -327,28 +299,21 @@ impl Drop for StatePortal {
}
}
/// 根据 DMA-BUF 帧信息构建 FFmpeg DRM 帧描述符
///
/// 将 PipeWire 提供的 DMA-BUF 参数fd、偏移量、步长、修饰符等
/// 转换为 FFmpeg 的 AVDRMFrameDescriptor 结构体,用于零拷贝硬件导入。
fn build_drm_descriptor(frame: &PwDmaBufFrame) -> ffi::AVDRMFrameDescriptor {
let mut desc: ffi::AVDRMFrameDescriptor = unsafe { mem::zeroed() };
fn portal_encode_dimensions(width: u32, height: u32) -> (u32, u32) {
const TARGET_W: u32 = 2560;
const TARGET_H: u32 = 1440;
// DMA-BUF 对象层:一个 fd 对应一个内存对象
desc.nb_objects = 1;
desc.objects[0].fd = frame.fd.as_raw_fd();
desc.objects[0].size = 0; // 大小为 0 表示整个 fd
desc.objects[0].format_modifier = frame.modifier;
if width <= TARGET_W && height <= TARGET_H {
return (width & !1, height & !1);
}
// 像素格式层:单层单平面布局(如 XR24 格式)
desc.nb_layers = 1;
desc.layers[0].format = frame.format;
desc.layers[0].nb_planes = 1;
desc.layers[0].planes[0].object_index = 0;
desc.layers[0].planes[0].offset = frame.offset as isize;
desc.layers[0].planes[0].pitch = frame.stride as isize;
desc
let width_limited_h = ((height as u64) * (TARGET_W as u64) / (width as u64)) as u32;
if width_limited_h <= TARGET_H {
(TARGET_W & !1, width_limited_h & !1)
} else {
let height_limited_w = ((width as u64) * (TARGET_H as u64) / (height as u64)) as u32;
(height_limited_w & !1, TARGET_H & !1)
}
}
/// Convert PipeWire nanosecond PTS to encoder frame-number units.
@@ -372,6 +337,22 @@ fn resolve_drm_device(args: &Args) -> Result<Option<PathBuf>> {
Ok(None)
}
#[cfg(test)]
fn build_drm_descriptor(frame: &PwDmaBufFrame) -> ffmpeg_next::ffi::AVDRMFrameDescriptor {
let mut desc: ffmpeg_next::ffi::AVDRMFrameDescriptor = unsafe { std::mem::zeroed() };
desc.nb_objects = 1;
desc.objects[0].fd = frame.fd.as_raw_fd();
desc.objects[0].size = 0;
desc.objects[0].format_modifier = frame.modifier;
desc.nb_layers = 1;
desc.layers[0].format = frame.format;
desc.layers[0].nb_planes = 1;
desc.layers[0].planes[0].object_index = 0;
desc.layers[0].planes[0].offset = frame.offset as isize;
desc.layers[0].planes[0].pitch = frame.stride as isize;
desc
}
#[cfg(test)]
mod tests {
use super::*;
@@ -426,7 +407,10 @@ mod tests {
port: 0,
};
let result = resolve_drm_device(&args).unwrap();
assert_eq!(result, Some(std::path::PathBuf::from("/dev/dri/renderD128")));
assert_eq!(
result,
Some(std::path::PathBuf::from("/dev/dri/renderD128"))
);
}
#[test]