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:
dev-team 2026-10-23 10:02:41 +08:00
parent 881341be3a
commit e7472fa12f
3 changed files with 154 additions and 40 deletions

59
agent/src/bin/encbench.rs Normal file
View 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);
}

View File

@ -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) {

View File

@ -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));