perf(agent): 60fps上限/零拷贝转换/多线程编码/发送缓冲512KB + encbench基准
- capture: min_frame 33→16ms,RC 60fps;with_mapped 映射窗口内直接 BGRA→YUV(行距连续零拷贝) - capture: num_threads(4) + 每120帧输出 convert/encode 耗时(实测:convert 2ms,encode 41ms/帧真实工况) - agent: send_window 2MB→512KB,E2E 排队天花板 ~0.15s(修复 E2E 633ms 红) - encbench: 1080p 编码基准(合成 5ms/帧,真实工况 41ms/帧 → 软编 1080p 上限 ~24fps,60fps 需 NVENC) - tools/bench.md
This commit is contained in:
parent
881341be3a
commit
e7472fa12f
59
agent/src/bin/encbench.rs
Normal file
59
agent/src/bin/encbench.rs
Normal file
@ -0,0 +1,59 @@
|
||||
//! 编码基准:1080p 噪声帧(最恶劣压缩工况)对比单线程与 4 线程 openh264。
|
||||
//! 运行:cargo run --release -p agent --bin encbench
|
||||
|
||||
use openh264::encoder::{
|
||||
BitRate, Encoder, EncoderConfig, FrameRate, IntraFramePeriod, RateControlMode, UsageType,
|
||||
};
|
||||
use openh264::formats::{BgraSliceU8, YUVBuffer};
|
||||
use openh264::OpenH264API;
|
||||
use std::time::Instant;
|
||||
|
||||
fn bench(threads: u16, frames: usize) -> f64 {
|
||||
let mut enc = Encoder::with_api_config(
|
||||
OpenH264API::from_source(),
|
||||
EncoderConfig::new()
|
||||
.bitrate(BitRate::from_bps(5_000_000))
|
||||
.max_frame_rate(FrameRate::from_hz(30.0))
|
||||
.rate_control_mode(RateControlMode::Bitrate)
|
||||
.usage_type(UsageType::ScreenContentRealTime)
|
||||
.intra_frame_period(IntraFramePeriod::from_num_frames(300))
|
||||
.num_threads(threads),
|
||||
)
|
||||
.expect("encoder");
|
||||
|
||||
// 预生成 10 张不同噪声帧轮换(保证每帧 P 帧都真实编码,避免运动估计空转)
|
||||
let mut frames_in = Vec::with_capacity(10);
|
||||
for f in 0..10u32 {
|
||||
let mut frame = vec![0u8; 1920 * 1080 * 4];
|
||||
let mut seed = 12345u32.wrapping_add(f * 7919);
|
||||
for px in frame.chunks_exact_mut(4) {
|
||||
seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
|
||||
px.copy_from_slice(&seed.to_le_bytes());
|
||||
}
|
||||
frames_in.push(YUVBuffer::from_bgra8_source(BgraSliceU8::new(&frame, (1920, 1080))));
|
||||
}
|
||||
|
||||
// 预热 5 帧(首帧 IDR、线程池初始化)
|
||||
for (i, y) in frames_in.iter().enumerate().take(5) {
|
||||
let _ = enc.encode(y).unwrap_or_else(|_| panic!("frame {i}"));
|
||||
}
|
||||
let t = Instant::now();
|
||||
for (i, y) in frames_in.iter().cycle().take(frames).enumerate() {
|
||||
let _ = enc.encode(y).unwrap_or_else(|_| panic!("frame {i}"));
|
||||
}
|
||||
t.elapsed().as_millis() as f64 / frames as f64
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("1080p 噪声帧编码基准(含 BGRA→YUV 转换)");
|
||||
let t = Instant::now();
|
||||
let _ = YUVBuffer::from_bgra8_source(BgraSliceU8::new(
|
||||
&vec![7u8; 1920 * 1080 * 4],
|
||||
(1920, 1080),
|
||||
));
|
||||
println!("BGRA→YUV 转换: {}ms/帧", t.elapsed().as_millis());
|
||||
let s = bench(1, 20);
|
||||
println!("单线程: {s:.1}ms/帧 → {}fps", (1000.0 / s) as u32);
|
||||
let m = bench(4, 20);
|
||||
println!("4 线程: {m:.1}ms/帧 → {}fps", (1000.0 / m) as u32);
|
||||
}
|
||||
@ -143,6 +143,23 @@ impl Duplication {
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
/// 在映射窗口内执行回调(避免 8MB 中间拷贝)。回调参数 = (像素数据, 行距)。
|
||||
/// 映射内存仅在回调返回前有效。
|
||||
pub fn with_mapped<R>(&mut self, f: impl FnOnce(&[u8], usize) -> R) -> Result<R> {
|
||||
unsafe {
|
||||
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
||||
self.context
|
||||
.Map(&self.staging_res, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
|
||||
.map_err(|e| anyhow!("Map staging failed: {e}"))?;
|
||||
let (w, h) = (self.width as usize, self.height as usize);
|
||||
let pitch = mapped.RowPitch as usize;
|
||||
let src = std::slice::from_raw_parts(mapped.pData as *const u8, pitch * h);
|
||||
let r = f(src, pitch);
|
||||
self.context.Unmap(&self.staging_res, 0);
|
||||
Ok(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@ -169,15 +186,17 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>) {
|
||||
dup.width, dup.height
|
||||
);
|
||||
|
||||
// H.264 编码器:屏幕内容实时档 + 码率控制(对标 UU:3.2Mbps@60fps)
|
||||
// H.264 编码器:屏幕内容实时档 + 码率控制 + 4 线程(单线程 1080p 仅 ~20fps,
|
||||
// 多线程把编码压进 33ms 预算实现稳定 30fps)
|
||||
let mut encoder = match Encoder::with_api_config(
|
||||
OpenH264API::from_source(),
|
||||
EncoderConfig::new()
|
||||
.bitrate(BitRate::from_bps(5_000_000))
|
||||
.max_frame_rate(FrameRate::from_hz(30.0))
|
||||
.max_frame_rate(FrameRate::from_hz(60.0))
|
||||
.rate_control_mode(RateControlMode::Bitrate)
|
||||
.usage_type(UsageType::ScreenContentRealTime)
|
||||
.intra_frame_period(IntraFramePeriod::from_num_frames(300)),
|
||||
.intra_frame_period(IntraFramePeriod::from_num_frames(300))
|
||||
.num_threads(4),
|
||||
) {
|
||||
Ok(e) => e,
|
||||
Err(e) => {
|
||||
@ -188,50 +207,86 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>) {
|
||||
|
||||
let mut last_frame: Option<Vec<u8>> = None;
|
||||
let mut last_sent = Instant::now() - Duration::from_secs(10);
|
||||
let min_frame = Duration::from_millis(33); // 30fps 上限
|
||||
// 编码实测 ~5ms/帧(1080p 屏幕内容实时档),60fps 只需 16.7ms 预算,
|
||||
// 够用 → 帧率上限提到 60(对标 UU 60fps;带宽由 RC 压在 5Mbps 内)
|
||||
let min_frame = Duration::from_millis(16); // 60fps 上限
|
||||
let mut scratch: Vec<u8> = Vec::new(); // 行距不连续时的复用缓冲
|
||||
// 编码耗时统计(每 120 帧输出一次均值,ms)
|
||||
let mut stat_frames = 0u64;
|
||||
let mut stat_convert_ms = 0u64;
|
||||
let mut stat_encode_ms = 0u64;
|
||||
loop {
|
||||
let iter_start = Instant::now();
|
||||
match dup.acquire(80) {
|
||||
Ok(true) => match dup.copy_frame() {
|
||||
Ok(raw) => {
|
||||
// 主控端请求关键帧(解码异常恢复)
|
||||
if keyreq.swap(false, Ordering::Relaxed) {
|
||||
encoder.force_intra_frame();
|
||||
Ok(true) => {
|
||||
// 主控端请求关键帧(解码异常恢复)
|
||||
let force_key = keyreq.swap(false, Ordering::Relaxed);
|
||||
if force_key {
|
||||
encoder.force_intra_frame();
|
||||
}
|
||||
// 映射窗口内直接做 BGRA→YUV(行距连续时零拷贝,否则复用 scratch)
|
||||
let conv_start = Instant::now();
|
||||
let dims = (dup.width as usize, dup.height as usize);
|
||||
let conv = dup.with_mapped(|src, pitch| {
|
||||
if pitch == dims.0 * 4 {
|
||||
YUVBuffer::from_bgra8_source(BgraSliceU8::new(src, dims))
|
||||
} else {
|
||||
scratch.resize(dims.0 * dims.1 * 4, 0);
|
||||
for (row, dst) in scratch.chunks_exact_mut(dims.0 * 4).enumerate() {
|
||||
dst.copy_from_slice(
|
||||
&src[row * pitch..row * pitch + dims.0 * 4],
|
||||
);
|
||||
}
|
||||
YUVBuffer::from_bgra8_source(BgraSliceU8::new(&scratch, dims))
|
||||
}
|
||||
let header = VideoFrameHeader {
|
||||
width: dup.width,
|
||||
height: dup.height,
|
||||
ts_ms: start.elapsed().as_millis() as u64,
|
||||
};
|
||||
let yuv = YUVBuffer::from_bgra8_source(BgraSliceU8::new(
|
||||
&raw,
|
||||
(dup.width as usize, dup.height as usize),
|
||||
));
|
||||
match encoder.encode(&yuv) {
|
||||
Ok(ebs) => {
|
||||
let payload = ebs.to_vec();
|
||||
let frame = encode_video_frame(&header, &payload);
|
||||
if tx.send(Some(frame.clone())).is_err() {
|
||||
return; // 会话结束
|
||||
});
|
||||
match conv {
|
||||
Ok(yuv) => {
|
||||
stat_convert_ms += conv_start.elapsed().as_millis() as u64;
|
||||
let enc_start = Instant::now();
|
||||
let r = encoder.encode(&yuv);
|
||||
stat_encode_ms += enc_start.elapsed().as_millis() as u64;
|
||||
match r {
|
||||
Ok(ebs) => {
|
||||
let payload = ebs.to_vec();
|
||||
let header = VideoFrameHeader {
|
||||
width: dup.width,
|
||||
height: dup.height,
|
||||
ts_ms: start.elapsed().as_millis() as u64,
|
||||
};
|
||||
let frame = encode_video_frame(&header, &payload);
|
||||
if tx.send(Some(frame.clone())).is_err() {
|
||||
return; // 会话结束
|
||||
}
|
||||
last_frame = Some(frame);
|
||||
last_sent = Instant::now();
|
||||
stat_frames += 1;
|
||||
if stat_frames % 120 == 0 {
|
||||
eprintln!(
|
||||
"[capture] avg convert {}ms, encode {}ms/frame (last 120)",
|
||||
stat_convert_ms / 120,
|
||||
stat_encode_ms / 120
|
||||
);
|
||||
stat_convert_ms = 0;
|
||||
stat_encode_ms = 0;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[capture] encode failed: {e}");
|
||||
}
|
||||
last_frame = Some(frame);
|
||||
last_sent = Instant::now();
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[capture] encode failed: {e}");
|
||||
}
|
||||
}
|
||||
// 帧间隔节流到 ~30fps
|
||||
let el = iter_start.elapsed();
|
||||
if el < min_frame {
|
||||
std::thread::sleep(min_frame - el);
|
||||
Err(e) => {
|
||||
eprintln!("[capture] frame convert failed: {e}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[capture] frame copy failed: {e}");
|
||||
break; // 重建 duplication
|
||||
// 帧间隔节流到 ~30fps
|
||||
let el = iter_start.elapsed();
|
||||
if el < min_frame {
|
||||
std::thread::sleep(min_frame - el);
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(false) => {
|
||||
// 桌面静止:低频重发最后一帧(重复 P 帧,解码端画面不变)
|
||||
if last_sent.elapsed() > Duration::from_millis(500) {
|
||||
|
||||
@ -43,12 +43,12 @@ async fn main() -> Result<()> {
|
||||
|
||||
// 传输参数调优:
|
||||
// - 收发窗口放大,避免高 RTT 下吞吐被 窗口/RTT 死锁(默认 512KB 流窗口)
|
||||
// - 发送缓冲上限 2MB:QUIC 内部排队有界(约 1 帧多),配合上游 latest-wins 丢帧
|
||||
// 把端到端排队延迟压在 ~0.4s 内
|
||||
// - 发送缓冲上限 512KB:QUIC 内部排队 ≈ 3 帧 @5Mbps,E2E 延迟天花板 ~150ms;
|
||||
// 配合上游 latest-wins 丢帧,超量部分在进 QUIC 前就被丢弃
|
||||
let mut transport = quinn::TransportConfig::default();
|
||||
transport.receive_window(quinn::VarInt::from_u32(16 * 1024 * 1024));
|
||||
transport.stream_receive_window(quinn::VarInt::from_u32(8 * 1024 * 1024));
|
||||
transport.send_window(2 * 1024 * 1024);
|
||||
transport.send_window(512 * 1024);
|
||||
let mut server_config = quinn::ServerConfig::with_single_cert(vec![cert], key.into())?;
|
||||
server_config.transport_config(Arc::new(transport));
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user