fix(viewer): 帧管线 latest-wins 单槽(修 NVENC 60fps 下积压假死/HUD 停刷)+ GOP 周期自愈

- viewer: 无界 mpsc → Mutex 单槽 latest-wins。根因:NVENC 60fps 到达率 > 渲染
  吞吐(~55fps),无界队列无限积压 → 延迟线性增长 + 渲染 drain 阻塞消息泵 →
  窗口假死、HUD 停刷(软编 20fps 时代掩盖,NVENC 后暴露)
- viewer: 解码帧尺寸与窗口解耦(blit 逐行拷贝,防 NVENC 对齐尺寸不匹配);
  首帧打印解码尺寸诊断
- agent: NVENC gop_len 0xffffffff→240(60fps 下 4s 周期 IDR 自愈恢复点,
  配合 KeyFrameReq 即时恢复,消除长时间冻结锯齿)
- 回环实测:NVENC 4ms/帧满速、解码 1920x1080 匹配、14s 零错误
This commit is contained in:
dev-team 2026-10-23 10:31:30 +08:00
parent 82ec166f23
commit 9c72aecc3b
2 changed files with 46 additions and 22 deletions

View File

@ -201,7 +201,8 @@ impl NvencEncoder {
// CBR 桌面实时档:码率恒定,延迟可预测(对标 UU 3.2Mbps@60fps) // CBR 桌面实时档:码率恒定,延迟可预测(对标 UU 3.2Mbps@60fps)
config.preset_cfg.rc_params.rate_control_mode = NVencParamsRcMode::CBR; config.preset_cfg.rc_params.rate_control_mode = NVencParamsRcMode::CBR;
config.preset_cfg.rc_params.average_bit_rate = bitrate_bps; config.preset_cfg.rc_params.average_bit_rate = bitrate_bps;
config.preset_cfg.gop_len = 0xffff_ffff; // 无周期 IDR,仅按需(KeyFrameReq) config.preset_cfg.gop_len = 240; // 周期 IDR(60fps 下 4s):网络异常后的自愈恢复点,
// 配合 KeyFrameReq 即时恢复,避免长时间冻结
config.preset_cfg.frame_interval_p = 1; // 无 B 帧 config.preset_cfg.frame_interval_p = 1; // 无 B 帧
let init = InitParams { let init = InitParams {

View File

@ -241,13 +241,20 @@ fn session(
let (w, h) = (width as usize, height as usize); let (w, h) = (width as usize, height as usize);
let stats = Arc::new(Stats::default()); let stats = Arc::new(Stats::default());
// 读任务(流 1):H.264 解码 → RGB → mpsc;同时统计端到端延迟与流量 // 读任务(流 1):H.264 解码 → RGB → 单槽 latest-wins;同时统计端到端延迟与流量
let (frame_tx, frame_rx) = std::sync::mpsc::channel::<Vec<u8>>(); // 帧格式 (src_w, src_h, rgb);(0,0,_) = 解码失败占位,触发 KeyFrameReq
// 单槽而非无界队列:渲染跟不上 60fps 到达率时旧帧直接被覆盖,绝不积压
//(无界 mpsc 在 NVENC 60fps 下会无限积压 → 延迟线性增长 + 渲染 drain 时
// 长时间阻塞窗口消息泵 → 窗口假死、HUD 停刷新)
let latest: Arc<std::sync::Mutex<Option<(u32, u32, Vec<u8>)>>> =
Arc::new(std::sync::Mutex::new(None));
{ {
let stats = stats.clone(); let stats = stats.clone();
let latest = latest.clone();
let mut decoder = openh264::decoder::Decoder::new() let mut decoder = openh264::decoder::Decoder::new()
.map_err(|e| anyhow::anyhow!("decoder init failed: {e}"))?; .map_err(|e| anyhow::anyhow!("decoder init failed: {e}"))?;
tokio::spawn(async move { tokio::spawn(async move {
let mut size_logged = false;
loop { loop {
match common::read_video_frame(&mut recv).await { match common::read_video_frame(&mut recv).await {
Ok((hdr, payload)) => { Ok((hdr, payload)) => {
@ -275,10 +282,19 @@ fn session(
match decoder.decode(&payload) { match decoder.decode(&payload) {
Ok(Some(yuv)) => { Ok(Some(yuv)) => {
let (dw, dh) = yuv.dimensions(); let (dw, dh) = yuv.dimensions();
let mut rgb = vec![0u8; dw * dh * 3]; let (dw, dh) = (dw as u32, dh as u32);
if !size_logged {
size_logged = true;
eprintln!(
"[viewer] decoded frame {dw}x{dh} (window {}x{})",
hdr.width, hdr.height
);
}
let mut rgb = vec![0u8; dw as usize * dh as usize * 3];
yuv.write_rgb8(&mut rgb); yuv.write_rgb8(&mut rgb);
if frame_tx.send(rgb).is_err() { // latest-wins:直接覆盖旧帧,无积压
break; if let Ok(mut slot) = latest.lock() {
*slot = Some((dw, dh, rgb));
} }
} }
Ok(None) => {} Ok(None) => {}
@ -286,7 +302,9 @@ fn session(
// 解码异常:请求被控端强制关键帧 // 解码异常:请求被控端强制关键帧
stats.dec_fail.fetch_add(1, Ordering::Relaxed); stats.dec_fail.fetch_add(1, Ordering::Relaxed);
eprintln!("[viewer] decode failed: {e}, request keyframe"); eprintln!("[viewer] decode failed: {e}, request keyframe");
let _ = frame_tx.send(Vec::new()); // 空帧占位唤醒渲染循环 if let Ok(mut slot) = latest.lock() {
*slot = Some((0, 0, Vec::new()));
}
} }
} }
} }
@ -379,18 +397,15 @@ fn session(
push(&msg_tx, ControlMsg::Ping(now_ms())); push(&msg_tx, ControlMsg::Ping(now_ms()));
} }
// 1) 渲染最新帧(只保留最新,丢弃积压);空帧=解码异常,请求关键帧 // 1) 渲染最新帧(单槽 latest-wins:每迭代只取最新一帧,天然防积压)
loop { if let Ok(mut slot) = latest.lock() {
match frame_rx.try_recv() { if let Some((sw, sh, raw)) = slot.take() {
Ok(raw) => { if raw.is_empty() {
if raw.is_empty() { push(&msg_tx, ControlMsg::KeyFrameReq);
push(&msg_tx, ControlMsg::KeyFrameReq); } else if sw > 0 && sh > 0 {
} else if raw.len() == w * h * 3 { blit(&mut buffer, w, h, &raw, sw as usize, sh as usize);
blit(&mut buffer, &raw); dirty = true;
dirty = true;
}
} }
Err(_) => break,
} }
} }
@ -675,9 +690,17 @@ fn draw_hud(buffer: &mut [u32], w: usize, lines: &[(&str, &str, u32)]) {
} }
} }
/// RGB888 字节(openh264 解码输出)→ minifb 0RGB u32 缓冲 /// RGB888 字节(openh264 解码输出)→ minifb 0RGB u32 缓冲。
fn blit(buffer: &mut [u32], raw: &[u8]) { /// 支持源尺寸与窗口不同(解码器可能对齐到 16 倍数,如 1080→1088):
for (px, chunk) in buffer.iter_mut().zip(raw.chunks_exact(3)) { /// 逐行拷贝重叠区域,源小于窗口时右侧/下方留黑。
*px = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32; fn blit(buffer: &mut [u32], w: usize, h: usize, raw: &[u8], dw: usize, dh: usize) {
let cw = w.min(dw);
let ch = h.min(dh);
for row in 0..ch {
let src = &raw[row * dw * 3..row * dw * 3 + cw * 3];
let dst = &mut buffer[row * w..row * w + cw];
for (px, chunk) in dst.iter_mut().zip(src.chunks_exact(3)) {
*px = ((chunk[0] as u32) << 16) | ((chunk[1] as u32) << 8) | chunk[2] as u32;
}
} }
} }