perf: eliminate GPU pipeline stalls and CPU software color conversion on Windows (v0.2.10)
This commit is contained in:
parent
861c045a58
commit
8e8560745c
9 changed files with 227 additions and 145 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -3700,7 +3700,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "scythe"
|
||||
version = "0.2.9"
|
||||
version = "0.2.10"
|
||||
dependencies = [
|
||||
"ashpd",
|
||||
"cpal",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "scythe"
|
||||
version = "0.2.9"
|
||||
version = "0.2.10"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|||
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<dyn std::error::Error + Send + Sync>> {
|
|||
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 {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<u8>,
|
||||
data: std::sync::Arc<Vec<u8>>,
|
||||
},
|
||||
/// Zero-copy DirectX 11 Texture handle. Used by Windows DXGI capture.
|
||||
#[cfg(target_os = "windows")]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<u8>,
|
||||
buffer_pool: Vec<Option<Arc<Vec<u8>>>>,
|
||||
last_frame: Option<Arc<Vec<u8>>>,
|
||||
}
|
||||
|
||||
#[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<ID3D11Texture2D> = 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<ID3D11Texture2D> = None;
|
||||
let mut tex1: Option<ID3D11Texture2D> = 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::<IDXGIOutput1>() {
|
||||
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<ID3D11Texture2D> = 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<ID3D11Texture2D> = None;
|
||||
let mut tex1: Option<ID3D11Texture2D> = 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<IDXGIResource> = 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]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
197
src/encoder.rs
197
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());
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue