perf: eliminate GPU pipeline stalls and CPU software color conversion on Windows (v0.2.10)
Some checks are pending
Release & Packaging / Build Linux Packages (.tar.gz, .deb, .rpm, .AppImage) (push) Waiting to run
Release & Packaging / Build Windows Binaries (.exe) (push) Waiting to run
Release & Packaging / Publish GitHub Release (push) Blocked by required conditions
Some checks are pending
Release & Packaging / Build Linux Packages (.tar.gz, .deb, .rpm, .AppImage) (push) Waiting to run
Release & Packaging / Build Windows Binaries (.exe) (push) Waiting to run
Release & Packaging / Publish GitHub Release (push) Blocked by required conditions
This commit is contained in:
parent
861c045a58
commit
c5b15888f0
9 changed files with 227 additions and 145 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -3700,7 +3700,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scythe"
|
name = "scythe"
|
||||||
version = "0.2.9"
|
version = "0.2.10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ashpd",
|
"ashpd",
|
||||||
"cpal",
|
"cpal",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "scythe"
|
name = "scythe"
|
||||||
version = "0.2.9"
|
version = "0.2.10"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
|
|
|
||||||
|
|
@ -307,7 +307,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
Ok(f) => f,
|
Ok(f) => f,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
Frame::Raw {
|
Frame::Raw {
|
||||||
data: vec![0u8; 1920 * 1080 * 4],
|
data: std::sync::Arc::new(vec![0u8; 1920 * 1080 * 4]),
|
||||||
width: 1920,
|
width: 1920,
|
||||||
height: 1080,
|
height: 1080,
|
||||||
stride: 1920 * 4,
|
stride: 1920 * 4,
|
||||||
|
|
@ -685,10 +685,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
Ok(Vec::new())
|
Ok(Vec::new())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let cached = encoder.encode_cached_frame(pts);
|
match encoder.encode_cached_frame(pts) {
|
||||||
match cached {
|
Ok(pkts) => Ok(pkts),
|
||||||
Ok(pkts) if !pkts.is_empty() => Ok(pkts),
|
Err(_) => {
|
||||||
_ => {
|
|
||||||
if let Some(ref f) = latest_frame {
|
if let Some(ref f) = latest_frame {
|
||||||
encoder.encode_frame(f, pts)
|
encoder.encode_frame(f, pts)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ impl FrameSource for MockCapture {
|
||||||
width: self.width,
|
width: self.width,
|
||||||
height: self.height,
|
height: self.height,
|
||||||
stride: self.width * 4,
|
stride: self.width * 4,
|
||||||
data,
|
data: std::sync::Arc::new(data),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ pub mod x11;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub mod windows;
|
pub mod windows;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Frame {
|
pub enum Frame {
|
||||||
/// Zero-copy DMA-BUF file descriptor. Used by Wayland/PipeWire on Linux.
|
/// Zero-copy DMA-BUF file descriptor. Used by Wayland/PipeWire on Linux.
|
||||||
DmaBuf {
|
DmaBuf {
|
||||||
|
|
@ -29,7 +29,7 @@ pub enum Frame {
|
||||||
width: u32,
|
width: u32,
|
||||||
height: u32,
|
height: u32,
|
||||||
stride: u32,
|
stride: u32,
|
||||||
data: Vec<u8>,
|
data: std::sync::Arc<Vec<u8>>,
|
||||||
},
|
},
|
||||||
/// Zero-copy DirectX 11 Texture handle. Used by Windows DXGI capture.
|
/// Zero-copy DirectX 11 Texture handle. Used by Windows DXGI capture.
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ impl PipeWireStream {
|
||||||
width: user_data.format.size().width,
|
width: user_data.format.size().width,
|
||||||
height: user_data.format.size().height,
|
height: user_data.format.size().height,
|
||||||
stride,
|
stride,
|
||||||
data: slice.to_vec(),
|
data: std::sync::Arc::new(slice.to_vec()),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -9,17 +9,21 @@ use windows::{
|
||||||
Win32::System::Com::*,
|
Win32::System::Com::*,
|
||||||
};
|
};
|
||||||
use crate::capture::{Frame, FrameSource};
|
use crate::capture::{Frame, FrameSource};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub struct WindowsCapture {
|
pub struct WindowsCapture {
|
||||||
device: ID3D11Device,
|
device: ID3D11Device,
|
||||||
context: ID3D11DeviceContext,
|
context: ID3D11DeviceContext,
|
||||||
duplication: IDXGIOutputDuplication,
|
duplication: IDXGIOutputDuplication,
|
||||||
staging_texture: ID3D11Texture2D,
|
staging_textures: [ID3D11Texture2D; 2],
|
||||||
|
staging_idx: usize,
|
||||||
|
has_staged_frame: bool,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub last_stride: 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")]
|
#[cfg(target_os = "windows")]
|
||||||
|
|
@ -92,7 +96,7 @@ impl WindowsCapture {
|
||||||
// Initialize Desktop Duplication
|
// Initialize Desktop Duplication
|
||||||
let duplication = output1.DuplicateOutput(&device)?;
|
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 {
|
let staging_desc = D3D11_TEXTURE2D_DESC {
|
||||||
Width: width,
|
Width: width,
|
||||||
Height: height,
|
Height: height,
|
||||||
|
|
@ -106,21 +110,29 @@ impl WindowsCapture {
|
||||||
MiscFlags: 0,
|
MiscFlags: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut staging_texture: Option<ID3D11Texture2D> = None;
|
let mut tex0: Option<ID3D11Texture2D> = None;
|
||||||
device.CreateTexture2D(&staging_desc, None, Some(&mut staging_texture))?;
|
let mut tex1: Option<ID3D11Texture2D> = None;
|
||||||
let staging_texture = staging_texture.ok_or("Failed to create D3D11 staging texture")?;
|
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 {
|
Ok(Self {
|
||||||
device,
|
device,
|
||||||
context,
|
context,
|
||||||
duplication,
|
duplication,
|
||||||
staging_texture,
|
staging_textures,
|
||||||
|
staging_idx: 0,
|
||||||
|
has_staged_frame: false,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
last_stride: width * 4,
|
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(output1) = output.cast::<IDXGIOutput1>() {
|
||||||
if let Ok(dup) = output1.DuplicateOutput(&self.device) {
|
if let Ok(dup) = output1.DuplicateOutput(&self.device) {
|
||||||
self.duplication = dup;
|
self.duplication = dup;
|
||||||
|
self.has_staged_frame = false;
|
||||||
|
self.staging_idx = 0;
|
||||||
if let Ok(desc) = output1.GetDesc() {
|
if let Ok(desc) = output1.GetDesc() {
|
||||||
let new_w = (desc.DesktopCoordinates.right - desc.DesktopCoordinates.left).unsigned_abs();
|
let new_w = (desc.DesktopCoordinates.right - desc.DesktopCoordinates.left).unsigned_abs();
|
||||||
let new_h = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top).unsigned_abs();
|
let new_h = (desc.DesktopCoordinates.bottom - desc.DesktopCoordinates.top).unsigned_abs();
|
||||||
|
|
@ -141,6 +155,8 @@ impl WindowsCapture {
|
||||||
self.width = new_w;
|
self.width = new_w;
|
||||||
self.height = new_h;
|
self.height = new_h;
|
||||||
self.last_stride = new_w * 4;
|
self.last_stride = new_w * 4;
|
||||||
|
self.buffer_pool.clear();
|
||||||
|
self.last_frame = None;
|
||||||
let staging_desc = D3D11_TEXTURE2D_DESC {
|
let staging_desc = D3D11_TEXTURE2D_DESC {
|
||||||
Width: new_w,
|
Width: new_w,
|
||||||
Height: new_h,
|
Height: new_h,
|
||||||
|
|
@ -153,10 +169,14 @@ impl WindowsCapture {
|
||||||
CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32,
|
CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32,
|
||||||
MiscFlags: 0,
|
MiscFlags: 0,
|
||||||
};
|
};
|
||||||
let mut new_staging: Option<ID3D11Texture2D> = None;
|
let mut tex0: Option<ID3D11Texture2D> = None;
|
||||||
if self.device.CreateTexture2D(&staging_desc, None, Some(&mut new_staging)).is_ok()
|
let mut tex1: Option<ID3D11Texture2D> = None;
|
||||||
&& let Some(st) = new_staging {
|
let ok0 = self.device.CreateTexture2D(&staging_desc, None, Some(&mut tex0)).is_ok();
|
||||||
self.staging_texture = st;
|
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 frame_info = DXGI_OUTDUPL_FRAME_INFO::default();
|
||||||
let mut desktop_resource: Option<IDXGIResource> = None;
|
let mut desktop_resource: Option<IDXGIResource> = None;
|
||||||
|
|
||||||
for _ in 0..5 {
|
for _ in 0..10 {
|
||||||
match self.duplication.AcquireNextFrame(16, &mut frame_info, &mut desktop_resource) {
|
match self.duplication.AcquireNextFrame(25, &mut frame_info, &mut desktop_resource) {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
if let Some(resource) = desktop_resource {
|
if let Some(resource) = desktop_resource {
|
||||||
let texture: ID3D11Texture2D = resource.cast()?;
|
let texture: ID3D11Texture2D = resource.cast()?;
|
||||||
|
|
||||||
// Copy GPU desktop texture to staging texture accessible by CPU
|
let write_idx = self.staging_idx;
|
||||||
self.context.CopyResource(&self.staging_texture, &texture);
|
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
|
// Immediately release the desktop frame back to the DWM compositor
|
||||||
let _ = self.duplication.ReleaseFrame();
|
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();
|
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;
|
let stride = mapped.RowPitch as usize;
|
||||||
self.last_stride = mapped.RowPitch;
|
self.last_stride = mapped.RowPitch;
|
||||||
let total_bytes = stride * self.height as usize;
|
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 {
|
return Ok(Frame::Raw {
|
||||||
width: self.width,
|
width: self.width,
|
||||||
height: self.height,
|
height: self.height,
|
||||||
stride: stride as u32,
|
stride: stride as u32,
|
||||||
data: self.last_frame.clone(),
|
data: to_send,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let _ = self.duplication.ReleaseFrame();
|
let _ = self.duplication.ReleaseFrame();
|
||||||
}
|
}
|
||||||
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => {
|
Err(e) if e.code() == DXGI_ERROR_WAIT_TIMEOUT => {
|
||||||
if !self.last_frame.is_empty() {
|
// Desktop static / no new frame presented. Sleep briefly and wait.
|
||||||
return Ok(Frame::Raw {
|
// Do NOT allocate duplicate frames!
|
||||||
width: self.width,
|
std::thread::sleep(std::time::Duration::from_millis(3));
|
||||||
height: self.height,
|
continue;
|
||||||
stride: self.last_stride,
|
|
||||||
data: self.last_frame.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(8));
|
|
||||||
}
|
}
|
||||||
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST
|
Err(e) if e.code() == DXGI_ERROR_ACCESS_LOST
|
||||||
|| e.code() == DXGI_ERROR_ACCESS_DENIED
|
|| e.code() == DXGI_ERROR_ACCESS_DENIED
|
||||||
|| e.code() == DXGI_ERROR_INVALID_CALL => {
|
|| e.code() == DXGI_ERROR_INVALID_CALL => {
|
||||||
let _ = self.duplication.ReleaseFrame();
|
let _ = self.duplication.ReleaseFrame();
|
||||||
let _ = self.reinit_duplication();
|
let _ = self.reinit_duplication();
|
||||||
if !self.last_frame.is_empty() {
|
std::thread::sleep(std::time::Duration::from_millis(20));
|
||||||
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));
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let _ = self.duplication.ReleaseFrame();
|
let _ = self.duplication.ReleaseFrame();
|
||||||
let _ = self.reinit_duplication();
|
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));
|
std::thread::sleep(std::time::Duration::from_millis(16));
|
||||||
return Err(Box::new(e));
|
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 {
|
Ok(Frame::Raw {
|
||||||
width: self.width,
|
width: self.width,
|
||||||
height: self.height,
|
height: self.height,
|
||||||
stride: self.last_stride,
|
stride: self.last_stride,
|
||||||
data: self.last_frame.clone(),
|
data: Arc::clone(last),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(Frame::Raw {
|
Ok(Frame::Raw {
|
||||||
width: self.width,
|
width: self.width,
|
||||||
height: self.height,
|
height: self.height,
|
||||||
stride: self.last_stride,
|
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,
|
width: self.width as u32,
|
||||||
height: self.height as u32,
|
height: self.height as u32,
|
||||||
stride,
|
stride,
|
||||||
data,
|
data: std::sync::Arc::new(data),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,7 +125,7 @@ impl FrameSource for X11Capture {
|
||||||
width: self.width as u32,
|
width: self.width as u32,
|
||||||
height: self.height as u32,
|
height: self.height as u32,
|
||||||
stride,
|
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,
|
width as i32,
|
||||||
height as i32,
|
height as i32,
|
||||||
AVPixelFormat::AV_PIX_FMT_NV12,
|
AVPixelFormat::AV_PIX_FMT_NV12,
|
||||||
2,
|
1,
|
||||||
ptr::null_mut(),
|
ptr::null_mut(),
|
||||||
ptr::null_mut(),
|
ptr::null_mut(),
|
||||||
ptr::null_mut(),
|
ptr::null_mut(),
|
||||||
|
|
@ -384,6 +384,7 @@ pub struct WindowsHwEncoder {
|
||||||
sws_ctx: *mut SwsContext,
|
sws_ctx: *mut SwsContext,
|
||||||
sw_frame: *mut AVFrame,
|
sw_frame: *mut AVFrame,
|
||||||
pub has_frame: bool,
|
pub has_frame: bool,
|
||||||
|
native_bgr: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
|
|
@ -422,6 +423,7 @@ impl WindowsHwEncoder {
|
||||||
|
|
||||||
let mut opened_codec_ctx: *mut AVCodecContext = ptr::null_mut();
|
let mut opened_codec_ctx: *mut AVCodecContext = ptr::null_mut();
|
||||||
let mut selected_desc = String::new();
|
let mut selected_desc = String::new();
|
||||||
|
let mut is_native_bgr = false;
|
||||||
|
|
||||||
for (name, desc) in candidates {
|
for (name, desc) in candidates {
|
||||||
let c = avcodec_find_encoder_by_name(name.as_ptr());
|
let c = avcodec_find_encoder_by_name(name.as_ptr());
|
||||||
|
|
@ -429,60 +431,77 @@ impl WindowsHwEncoder {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ctx = avcodec_alloc_context3(c);
|
// Prioritize native BGR0 for hardware encoders (NVENC, AMF) to eliminate CPU color conversion
|
||||||
if ctx.is_null() {
|
let formats_to_try: &[AVPixelFormat] = if desc.contains("NVENC") || desc.contains("AMF") {
|
||||||
continue;
|
&[AVPixelFormat::AV_PIX_FMT_BGR0, AVPixelFormat::AV_PIX_FMT_NV12]
|
||||||
}
|
|
||||||
|
|
||||||
(*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);
|
|
||||||
} else if desc.contains("QuickSync") {
|
} else if desc.contains("QuickSync") {
|
||||||
(*ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_NV12;
|
&[AVPixelFormat::AV_PIX_FMT_NV12]
|
||||||
let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"veryfast".as_ptr(), 0);
|
|
||||||
} else {
|
} else {
|
||||||
(*ctx).pix_fmt = AVPixelFormat::AV_PIX_FMT_YUV420P;
|
&[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);
|
|
||||||
|
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 !opened_codec_ctx.is_null() {
|
||||||
if ret >= 0 {
|
|
||||||
println!("Successfully initialized Windows video encoder: {}", desc);
|
|
||||||
opened_codec_ctx = ctx;
|
|
||||||
selected_desc = desc.to_string();
|
|
||||||
break;
|
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());
|
return Err("Failed to allocate frame buffer for encoder".into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let sws_ctx = sws_getContext(
|
// Only initialize sws_ctx if color conversion is required (e.g. QSV or CPU fallback)
|
||||||
width as i32,
|
let sws_ctx = if !is_native_bgr {
|
||||||
height as i32,
|
sws_getContext(
|
||||||
AVPixelFormat::AV_PIX_FMT_BGRA,
|
width as i32,
|
||||||
width as i32,
|
height as i32,
|
||||||
height as i32,
|
AVPixelFormat::AV_PIX_FMT_BGRA,
|
||||||
(*codec_ctx).pix_fmt,
|
width as i32,
|
||||||
2,
|
height as i32,
|
||||||
ptr::null_mut(),
|
(*codec_ctx).pix_fmt,
|
||||||
ptr::null_mut(),
|
1, // SWS_FAST_BILINEAR for SIMD acceleration
|
||||||
ptr::null_mut(),
|
ptr::null_mut(),
|
||||||
);
|
ptr::null_mut(),
|
||||||
|
ptr::null_mut(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ptr::null_mut()
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
codec_ctx,
|
codec_ctx,
|
||||||
|
|
@ -524,6 +548,7 @@ impl WindowsHwEncoder {
|
||||||
sws_ctx,
|
sws_ctx,
|
||||||
sw_frame,
|
sw_frame,
|
||||||
has_frame: false,
|
has_frame: false,
|
||||||
|
native_bgr: is_native_bgr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -537,22 +562,43 @@ impl WindowsHwEncoder {
|
||||||
unsafe {
|
unsafe {
|
||||||
match frame {
|
match frame {
|
||||||
Frame::Raw { width: _, height, stride, data } => {
|
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 {
|
if av_frame_make_writable(self.sw_frame) < 0 {
|
||||||
let _ = av_frame_get_buffer(self.sw_frame, 32);
|
let _ = av_frame_get_buffer(self.sw_frame, 32);
|
||||||
}
|
}
|
||||||
|
|
||||||
sws_scale(
|
if self.native_bgr {
|
||||||
self.sws_ctx,
|
// Direct hardware encoding: zero CPU color conversion
|
||||||
src_data.as_ptr(),
|
let dst_data = (*self.sw_frame).data[0];
|
||||||
src_linesize.as_ptr(),
|
let dst_linesize = (*self.sw_frame).linesize[0] as usize;
|
||||||
0,
|
let src_stride = *stride as usize;
|
||||||
*height as i32,
|
let row_bytes = ((*self.sw_frame).width as usize * 4).min(src_stride).min(dst_linesize);
|
||||||
(*self.sw_frame).data.as_mut_ptr(),
|
|
||||||
(*self.sw_frame).linesize.as_mut_ptr(),
|
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;
|
(*self.sw_frame).pts = pts;
|
||||||
|
|
||||||
|
|
@ -569,7 +615,6 @@ impl WindowsHwEncoder {
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
Frame::D3D11Texture { handle: _, .. } => {
|
Frame::D3D11Texture { handle: _, .. } => {
|
||||||
// Zero-copy D3D11 frame submission
|
|
||||||
(*self.sw_frame).pts = pts;
|
(*self.sw_frame).pts = pts;
|
||||||
|
|
||||||
if avcodec_send_frame(self.codec_ctx, self.sw_frame) >= 0 {
|
if avcodec_send_frame(self.codec_ctx, self.sw_frame) >= 0 {
|
||||||
|
|
@ -850,7 +895,7 @@ mod tests {
|
||||||
width: 1280,
|
width: 1280,
|
||||||
height: 720,
|
height: 720,
|
||||||
stride: 1280 * 4,
|
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");
|
let pkts = enc.encode_frame(&blank_frame, 0).expect("Encode frame");
|
||||||
println!("Encoded {} packets from first frame", pkts.len());
|
println!("Encoded {} packets from first frame", pkts.len());
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue