diff --git a/Cargo.lock b/Cargo.lock index b3a21f4..641b2a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3700,7 +3700,7 @@ dependencies = [ [[package]] name = "scythe" -version = "0.2.9" +version = "0.2.10" dependencies = [ "ashpd", "cpal", diff --git a/Cargo.toml b/Cargo.toml index 176ac1a..ab91e40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "scythe" -version = "0.2.9" +version = "0.2.10" edition = "2024" [[bin]] diff --git a/src/bin/scythe-daemon.rs b/src/bin/scythe-daemon.rs index c68907b..e66c52b 100644 --- a/src/bin/scythe-daemon.rs +++ b/src/bin/scythe-daemon.rs @@ -307,7 +307,7 @@ async fn main() -> Result<(), Box> { Ok(f) => f, Err(_) => { Frame::Raw { - data: vec![0u8; 1920 * 1080 * 4], + data: std::sync::Arc::new(vec![0u8; 1920 * 1080 * 4]), width: 1920, height: 1080, stride: 1920 * 4, @@ -685,10 +685,9 @@ async fn main() -> Result<(), Box> { Ok(Vec::new()) } } else { - let cached = encoder.encode_cached_frame(pts); - match cached { - Ok(pkts) if !pkts.is_empty() => Ok(pkts), - _ => { + match encoder.encode_cached_frame(pts) { + Ok(pkts) => Ok(pkts), + Err(_) => { if let Some(ref f) = latest_frame { encoder.encode_frame(f, pts) } else { diff --git a/src/capture/mock.rs b/src/capture/mock.rs index e5208bf..4bb6e30 100644 --- a/src/capture/mock.rs +++ b/src/capture/mock.rs @@ -39,7 +39,7 @@ impl FrameSource for MockCapture { width: self.width, height: self.height, stride: self.width * 4, - data, + data: std::sync::Arc::new(data), }) } } diff --git a/src/capture/mod.rs b/src/capture/mod.rs index 95f9540..d46c022 100644 --- a/src/capture/mod.rs +++ b/src/capture/mod.rs @@ -12,7 +12,7 @@ pub mod x11; #[cfg(target_os = "windows")] pub mod windows; -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum Frame { /// Zero-copy DMA-BUF file descriptor. Used by Wayland/PipeWire on Linux. DmaBuf { @@ -29,7 +29,7 @@ pub enum Frame { width: u32, height: u32, stride: u32, - data: Vec, + data: std::sync::Arc>, }, /// Zero-copy DirectX 11 Texture handle. Used by Windows DXGI capture. #[cfg(target_os = "windows")] diff --git a/src/capture/wayland_stream.rs b/src/capture/wayland_stream.rs index 39532ba..db9802c 100644 --- a/src/capture/wayland_stream.rs +++ b/src/capture/wayland_stream.rs @@ -91,7 +91,7 @@ impl PipeWireStream { width: user_data.format.size().width, height: user_data.format.size().height, stride, - data: slice.to_vec(), + data: std::sync::Arc::new(slice.to_vec()), } } else { return; diff --git a/src/capture/windows.rs b/src/capture/windows.rs index c03f73e..235432c 100644 --- a/src/capture/windows.rs +++ b/src/capture/windows.rs @@ -9,17 +9,21 @@ use windows::{ Win32::System::Com::*, }; use crate::capture::{Frame, FrameSource}; +use std::sync::Arc; #[cfg(target_os = "windows")] pub struct WindowsCapture { device: ID3D11Device, context: ID3D11DeviceContext, duplication: IDXGIOutputDuplication, - staging_texture: ID3D11Texture2D, + staging_textures: [ID3D11Texture2D; 2], + staging_idx: usize, + has_staged_frame: bool, pub width: u32, pub height: u32, pub last_stride: u32, - last_frame: Vec, + buffer_pool: Vec>>>, + last_frame: Option>>, } #[cfg(target_os = "windows")] @@ -92,7 +96,7 @@ impl WindowsCapture { // Initialize Desktop Duplication let duplication = output1.DuplicateOutput(&device)?; - // Allocate a CPU-accessible staging texture for hardware frame reading + // Allocate double-buffered CPU-accessible staging textures for asynchronous zero-stall GPU readback let staging_desc = D3D11_TEXTURE2D_DESC { Width: width, Height: height, @@ -106,21 +110,29 @@ impl WindowsCapture { MiscFlags: 0, }; - let mut staging_texture: Option = None; - device.CreateTexture2D(&staging_desc, None, Some(&mut staging_texture))?; - let staging_texture = staging_texture.ok_or("Failed to create D3D11 staging texture")?; + let mut tex0: Option = None; + let mut tex1: Option = None; + device.CreateTexture2D(&staging_desc, None, Some(&mut tex0))?; + device.CreateTexture2D(&staging_desc, None, Some(&mut tex1))?; + let staging_textures = [ + tex0.ok_or("Failed to create D3D11 staging texture 0")?, + tex1.ok_or("Failed to create D3D11 staging texture 1")?, + ]; - println!("Windows DXGI Hardware Desktop Duplication active: {}x{}", width, height); + println!("Windows DXGI Hardware Desktop Duplication active (asynchronous double-buffered): {}x{}", width, height); Ok(Self { device, context, duplication, - staging_texture, + staging_textures, + staging_idx: 0, + has_staged_frame: false, width, height, last_stride: width * 4, - last_frame: Vec::new(), + buffer_pool: Vec::with_capacity(4), + last_frame: None, }) } } @@ -134,6 +146,8 @@ impl WindowsCapture { if let Ok(output1) = output.cast::() { if let Ok(dup) = output1.DuplicateOutput(&self.device) { self.duplication = dup; + self.has_staged_frame = false; + self.staging_idx = 0; if let Ok(desc) = output1.GetDesc() { let new_w = (desc.DesktopCoordinates.right - desc.DesktopCoordinates.left).unsigned_abs(); let new_h = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top).unsigned_abs(); @@ -141,6 +155,8 @@ impl WindowsCapture { self.width = new_w; self.height = new_h; self.last_stride = new_w * 4; + self.buffer_pool.clear(); + self.last_frame = None; let staging_desc = D3D11_TEXTURE2D_DESC { Width: new_w, Height: new_h, @@ -153,10 +169,14 @@ impl WindowsCapture { CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32, MiscFlags: 0, }; - let mut new_staging: Option = None; - if self.device.CreateTexture2D(&staging_desc, None, Some(&mut new_staging)).is_ok() - && let Some(st) = new_staging { - self.staging_texture = st; + let mut tex0: Option = None; + let mut tex1: Option = None; + let ok0 = self.device.CreateTexture2D(&staging_desc, None, Some(&mut tex0)).is_ok(); + let ok1 = self.device.CreateTexture2D(&staging_desc, None, Some(&mut tex1)).is_ok(); + if ok0 && ok1 { + if let (Some(t0), Some(t1)) = (tex0, tex1) { + self.staging_textures = [t0, t1]; + } } } } @@ -177,100 +197,118 @@ impl FrameSource for WindowsCapture { let mut frame_info = DXGI_OUTDUPL_FRAME_INFO::default(); let mut desktop_resource: Option = None; - for _ in 0..5 { - match self.duplication.AcquireNextFrame(16, &mut frame_info, &mut desktop_resource) { + for _ in 0..10 { + match self.duplication.AcquireNextFrame(25, &mut frame_info, &mut desktop_resource) { Ok(()) => { if let Some(resource) = desktop_resource { let texture: ID3D11Texture2D = resource.cast()?; - // Copy GPU desktop texture to staging texture accessible by CPU - self.context.CopyResource(&self.staging_texture, &texture); + let write_idx = self.staging_idx; + let read_idx = 1 - self.staging_idx; + + // Asynchronously copy GPU desktop texture into staging texture[write_idx] + self.context.CopyResource(&self.staging_textures[write_idx], &texture); // Immediately release the desktop frame back to the DWM compositor let _ = self.duplication.ReleaseFrame(); + // Use ping-pong texture: + // On first frame, read write_idx directly. + // On subsequent frames, read read_idx (queued 1 frame ago, DMA already finished!) + let target_read = if !self.has_staged_frame { + self.has_staged_frame = true; + write_idx + } else { + read_idx + }; + self.staging_idx = 1 - self.staging_idx; + let mut mapped = D3D11_MAPPED_SUBRESOURCE::default(); - self.context.Map(&self.staging_texture, 0, D3D11_MAP_READ, 0, Some(&mut mapped))?; + self.context.Map(&self.staging_textures[target_read], 0, D3D11_MAP_READ, 0, Some(&mut mapped))?; let stride = mapped.RowPitch as usize; self.last_stride = mapped.RowPitch; let total_bytes = stride * self.height as usize; - if self.last_frame.len() != total_bytes { - self.last_frame.resize(total_bytes, 0); + + // Reusable zero-allocation buffer acquisition from pool + let mut arc_buf = None; + for slot in &mut self.buffer_pool { + if let Some(buf) = slot { + if Arc::strong_count(buf) == 1 { + arc_buf = slot.take(); + break; + } + } + } + let mut arc_buf = arc_buf.unwrap_or_else(|| Arc::new(vec![0u8; total_bytes])); + { + let vec_ref = Arc::get_mut(&mut arc_buf).unwrap(); + if vec_ref.len() != total_bytes { + vec_ref.resize(total_bytes, 0); + } + std::ptr::copy_nonoverlapping( + mapped.pData as *const u8, + vec_ref.as_mut_ptr(), + total_bytes, + ); + } + self.context.Unmap(&self.staging_textures[target_read], 0); + + let to_send = Arc::clone(&arc_buf); + self.last_frame = Some(Arc::clone(&arc_buf)); + + // Return to buffer pool + if self.buffer_pool.len() < 4 { + self.buffer_pool.push(Some(arc_buf)); } - std::ptr::copy_nonoverlapping( - mapped.pData as *const u8, - self.last_frame.as_mut_ptr(), - total_bytes, - ); - self.context.Unmap(&self.staging_texture, 0); return Ok(Frame::Raw { width: self.width, height: self.height, stride: stride as u32, - data: self.last_frame.clone(), + data: to_send, }); } let _ = self.duplication.ReleaseFrame(); } Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => { - if !self.last_frame.is_empty() { - return Ok(Frame::Raw { - width: self.width, - height: self.height, - stride: self.last_stride, - data: self.last_frame.clone(), - }); - } - std::thread::sleep(std::time::Duration::from_millis(8)); + // Desktop static / no new frame presented. Sleep briefly and wait. + // Do NOT allocate duplicate frames! + std::thread::sleep(std::time::Duration::from_millis(3)); + continue; } Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST || e.code() == DXGI_ERROR_ACCESS_DENIED || e.code() == DXGI_ERROR_INVALID_CALL => { let _ = self.duplication.ReleaseFrame(); let _ = self.reinit_duplication(); - if !self.last_frame.is_empty() { - return Ok(Frame::Raw { - width: self.width, - height: self.height, - stride: self.last_stride, - data: self.last_frame.clone(), - }); - } - std::thread::sleep(std::time::Duration::from_millis(30)); + std::thread::sleep(std::time::Duration::from_millis(20)); continue; } Err(e) => { let _ = self.duplication.ReleaseFrame(); let _ = self.reinit_duplication(); - if !self.last_frame.is_empty() { - return Ok(Frame::Raw { - width: self.width, - height: self.height, - stride: self.last_stride, - data: self.last_frame.clone(), - }); - } std::thread::sleep(std::time::Duration::from_millis(16)); return Err(Box::new(e)); } } } - if !self.last_frame.is_empty() { + // If desktop was completely idle across multiple iterations, + // return zero-copy reference to last frame so stream stays active without heap thrashing + if let Some(ref last) = self.last_frame { Ok(Frame::Raw { width: self.width, height: self.height, stride: self.last_stride, - data: self.last_frame.clone(), + data: Arc::clone(last), }) } else { Ok(Frame::Raw { width: self.width, height: self.height, stride: self.last_stride, - data: vec![0u8; (self.last_stride * self.height) as usize], + data: Arc::new(vec![0u8; (self.last_stride * self.height) as usize]), }) } } diff --git a/src/capture/x11.rs b/src/capture/x11.rs index 99306ba..045d267 100644 --- a/src/capture/x11.rs +++ b/src/capture/x11.rs @@ -103,7 +103,7 @@ impl FrameSource for X11Capture { width: self.width as u32, height: self.height as u32, stride, - data, + data: std::sync::Arc::new(data), }); } @@ -125,7 +125,7 @@ impl FrameSource for X11Capture { width: self.width as u32, height: self.height as u32, stride, - data, + data: std::sync::Arc::new(data), }) } } diff --git a/src/encoder.rs b/src/encoder.rs index 59b22cd..43625d3 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -135,7 +135,7 @@ impl VaapiEncoder { width as i32, height as i32, AVPixelFormat::AV_PIX_FMT_NV12, - 2, + 1, ptr::null_mut(), ptr::null_mut(), ptr::null_mut(), @@ -384,6 +384,7 @@ pub struct WindowsHwEncoder { sws_ctx: *mut SwsContext, sw_frame: *mut AVFrame, pub has_frame: bool, + native_bgr: bool, } #[cfg(target_os = "windows")] @@ -422,6 +423,7 @@ impl WindowsHwEncoder { let mut opened_codec_ctx: *mut AVCodecContext = ptr::null_mut(); let mut selected_desc = String::new(); + let mut is_native_bgr = false; for (name, desc) in candidates { let c = avcodec_find_encoder_by_name(name.as_ptr()); @@ -429,60 +431,77 @@ impl WindowsHwEncoder { continue; } - let ctx = avcodec_alloc_context3(c); - if ctx.is_null() { - continue; - } - - (*ctx).width = width as i32; - (*ctx).height = height as i32; - (*ctx).time_base = AVRational { num: 1, den: fps }; - (*ctx).framerate = AVRational { num: fps, den: 1 }; - (*ctx).gop_size = fps; - (*ctx).max_b_frames = 0; - - let rate = (bitrate_kbps as i64) * 1000; - (*ctx).bit_rate = rate; - (*ctx).rc_max_rate = rate * 3 / 2; - (*ctx).rc_buffer_size = (rate / 2) as i32; - (*ctx).qmin = 16; - (*ctx).qmax = 28; - let codec_id = (*c).id; - (*ctx).profile = if codec_id == AVCodecID::AV_CODEC_ID_HEVC { - FF_PROFILE_HEVC_MAIN - } else if codec_id == AVCodecID::AV_CODEC_ID_AV1 { - FF_PROFILE_UNKNOWN - } else { - FF_PROFILE_H264_HIGH - }; - (*ctx).flags |= AV_CODEC_FLAG_GLOBAL_HEADER as libc::c_int; - - if desc.contains("NVENC") { - (*ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_NV12; - let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"p1".as_ptr(), 0); - let _ = av_opt_set((*ctx).priv_data, c"tune".as_ptr(), c"ull".as_ptr(), 0); - let _ = av_opt_set((*ctx).priv_data, c"forced-idr".as_ptr(), c"1".as_ptr(), 0); - } else if desc.contains("AMF") { - (*ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_NV12; - let _ = av_opt_set((*ctx).priv_data, c"usage".as_ptr(), c"ultralowlatency".as_ptr(), 0); + // Prioritize native BGR0 for hardware encoders (NVENC, AMF) to eliminate CPU color conversion + let formats_to_try: &[AVPixelFormat] = if desc.contains("NVENC") || desc.contains("AMF") { + &[AVPixelFormat::AV_PIX_FMT_BGR0, AVPixelFormat::AV_PIX_FMT_NV12] } else if desc.contains("QuickSync") { - (*ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_NV12; - let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"veryfast".as_ptr(), 0); + &[AVPixelFormat::AV_PIX_FMT_NV12] } else { - (*ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_YUV420P; - let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"ultrafast".as_ptr(), 0); - let _ = av_opt_set((*ctx).priv_data, c"tune".as_ptr(), c"zerolatency".as_ptr(), 0); + &[AVPixelFormat::AV_PIX_FMT_YUV420P] + }; + + for &pix_fmt in formats_to_try { + let ctx = avcodec_alloc_context3(c); + if ctx.is_null() { + continue; + } + + (*ctx).width = width as i32; + (*ctx).height = height as i32; + (*ctx).time_base = AVRational { num: 1, den: fps }; + (*ctx).framerate = AVRational { num: fps, den: 1 }; + (*ctx).gop_size = fps; + (*ctx).max_b_frames = 0; + + let rate = (bitrate_kbps as i64) * 1000; + (*ctx).bit_rate = rate; + (*ctx).rc_max_rate = rate * 3 / 2; + (*ctx).rc_buffer_size = (rate / 2) as i32; + (*ctx).qmin = 16; + (*ctx).qmax = 28; + let codec_id = (*c).id; + (*ctx).profile = if codec_id == AVCodecID::AV_CODEC_ID_HEVC { + FF_PROFILE_HEVC_MAIN + } else if codec_id == AVCodecID::AV_CODEC_ID_AV1 { + FF_PROFILE_UNKNOWN + } else { + FF_PROFILE_H264_HIGH + }; + (*ctx).flags |= AV_CODEC_FLAG_GLOBAL_HEADER as libc::c_int; + (*ctx).pix_fmt = pix_fmt; + + if desc.contains("NVENC") { + let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"p1".as_ptr(), 0); + let _ = av_opt_set((*ctx).priv_data, c"tune".as_ptr(), c"ull".as_ptr(), 0); + let _ = av_opt_set((*ctx).priv_data, c"forced-idr".as_ptr(), c"1".as_ptr(), 0); + if pix_fmt == AVPixelFormat::AV_PIX_FMT_BGR0 { + let _ = av_opt_set((*ctx).priv_data, c"rgb_mode".as_ptr(), c"yuv420".as_ptr(), 0); + } + } else if desc.contains("AMF") { + let _ = av_opt_set((*ctx).priv_data, c"usage".as_ptr(), c"ultralowlatency".as_ptr(), 0); + } else if desc.contains("QuickSync") { + let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"veryfast".as_ptr(), 0); + } else { + let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"ultrafast".as_ptr(), 0); + let _ = av_opt_set((*ctx).priv_data, c"tune".as_ptr(), c"zerolatency".as_ptr(), 0); + } + + let ret = avcodec_open2(ctx, c, ptr::null_mut()); + if ret >= 0 { + let native = pix_fmt == AVPixelFormat::AV_PIX_FMT_BGR0; + println!("Successfully initialized Windows video encoder: {} (native hardware BGR: {})", desc, native); + opened_codec_ctx = ctx; + selected_desc = desc.to_string(); + is_native_bgr = native; + break; + } else { + let mut tmp = ctx; + avcodec_free_context(&mut tmp); + } } - let ret = avcodec_open2(ctx, c, ptr::null_mut()); - if ret >= 0 { - println!("Successfully initialized Windows video encoder: {}", desc); - opened_codec_ctx = ctx; - selected_desc = desc.to_string(); + if !opened_codec_ctx.is_null() { break; - } else { - let mut tmp = ctx; - avcodec_free_context(&mut tmp); } } @@ -505,18 +524,23 @@ impl WindowsHwEncoder { return Err("Failed to allocate frame buffer for encoder".into()); } - let sws_ctx = sws_getContext( - width as i32, - height as i32, - AVPixelFormat::AV_PIX_FMT_BGRA, - width as i32, - height as i32, - (*codec_ctx).pix_fmt, - 2, - ptr::null_mut(), - ptr::null_mut(), - ptr::null_mut(), - ); + // Only initialize sws_ctx if color conversion is required (e.g. QSV or CPU fallback) + let sws_ctx = if !is_native_bgr { + sws_getContext( + width as i32, + height as i32, + AVPixelFormat::AV_PIX_FMT_BGRA, + width as i32, + height as i32, + (*codec_ctx).pix_fmt, + 1, // SWS_FAST_BILINEAR for SIMD acceleration + ptr::null_mut(), + ptr::null_mut(), + ptr::null_mut(), + ) + } else { + ptr::null_mut() + }; Ok(Self { codec_ctx, @@ -524,6 +548,7 @@ impl WindowsHwEncoder { sws_ctx, sw_frame, has_frame: false, + native_bgr: is_native_bgr, }) } } @@ -537,22 +562,43 @@ impl WindowsHwEncoder { unsafe { match frame { Frame::Raw { width: _, height, stride, data } => { - let src_data = [data.as_ptr(), ptr::null(), ptr::null(), ptr::null()]; - let src_linesize = [*stride as i32, 0, 0, 0]; - if av_frame_make_writable(self.sw_frame) < 0 { let _ = av_frame_get_buffer(self.sw_frame, 32); } - sws_scale( - self.sws_ctx, - src_data.as_ptr(), - src_linesize.as_ptr(), - 0, - *height as i32, - (*self.sw_frame).data.as_mut_ptr(), - (*self.sw_frame).linesize.as_mut_ptr(), - ); + if self.native_bgr { + // Direct hardware encoding: zero CPU color conversion + let dst_data = (*self.sw_frame).data[0]; + let dst_linesize = (*self.sw_frame).linesize[0] as usize; + let src_stride = *stride as usize; + let row_bytes = ((*self.sw_frame).width as usize * 4).min(src_stride).min(dst_linesize); + + if dst_linesize == src_stride && row_bytes == src_stride { + std::ptr::copy_nonoverlapping( + data.as_ptr(), + dst_data, + row_bytes * *height as usize, + ); + } else { + for y in 0..(*height as usize) { + let src_row = data.as_ptr().add(y * src_stride); + let dst_row = dst_data.add(y * dst_linesize); + std::ptr::copy_nonoverlapping(src_row, dst_row, row_bytes); + } + } + } else if !self.sws_ctx.is_null() { + let src_data = [data.as_ptr(), ptr::null(), ptr::null(), ptr::null()]; + let src_linesize = [*stride as i32, 0, 0, 0]; + sws_scale( + self.sws_ctx, + src_data.as_ptr(), + src_linesize.as_ptr(), + 0, + *height as i32, + (*self.sw_frame).data.as_mut_ptr(), + (*self.sw_frame).linesize.as_mut_ptr(), + ); + } (*self.sw_frame).pts = pts; @@ -569,7 +615,6 @@ impl WindowsHwEncoder { } #[cfg(target_os = "windows")] Frame::D3D11Texture { handle: _, .. } => { - // Zero-copy D3D11 frame submission (*self.sw_frame).pts = pts; if avcodec_send_frame(self.codec_ctx, self.sw_frame) >= 0 { @@ -850,7 +895,7 @@ mod tests { width: 1280, height: 720, stride: 1280 * 4, - data: vec![0u8; 1280 * 720 * 4], + data: std::sync::Arc::new(vec![0u8; 1280 * 720 * 4]), }; let pkts = enc.encode_frame(&blank_frame, 0).expect("Encode frame"); println!("Encoded {} packets from first frame", pkts.len());