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:
parent
82ec166f23
commit
9c72aecc3b
@ -201,7 +201,8 @@ impl NvencEncoder {
|
||||
// CBR 桌面实时档:码率恒定,延迟可预测(对标 UU 3.2Mbps@60fps)
|
||||
config.preset_cfg.rc_params.rate_control_mode = NVencParamsRcMode::CBR;
|
||||
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 帧
|
||||
|
||||
let init = InitParams {
|
||||
|
||||
@ -241,13 +241,20 @@ fn session(
|
||||
let (w, h) = (width as usize, height as usize);
|
||||
let stats = Arc::new(Stats::default());
|
||||
|
||||
// 读任务(流 1):H.264 解码 → RGB → mpsc;同时统计端到端延迟与流量
|
||||
let (frame_tx, frame_rx) = std::sync::mpsc::channel::<Vec<u8>>();
|
||||
// 读任务(流 1):H.264 解码 → RGB → 单槽 latest-wins;同时统计端到端延迟与流量
|
||||
// 帧格式 (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 latest = latest.clone();
|
||||
let mut decoder = openh264::decoder::Decoder::new()
|
||||
.map_err(|e| anyhow::anyhow!("decoder init failed: {e}"))?;
|
||||
tokio::spawn(async move {
|
||||
let mut size_logged = false;
|
||||
loop {
|
||||
match common::read_video_frame(&mut recv).await {
|
||||
Ok((hdr, payload)) => {
|
||||
@ -275,10 +282,19 @@ fn session(
|
||||
match decoder.decode(&payload) {
|
||||
Ok(Some(yuv)) => {
|
||||
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);
|
||||
if frame_tx.send(rgb).is_err() {
|
||||
break;
|
||||
// latest-wins:直接覆盖旧帧,无积压
|
||||
if let Ok(mut slot) = latest.lock() {
|
||||
*slot = Some((dw, dh, rgb));
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
@ -286,7 +302,9 @@ fn session(
|
||||
// 解码异常:请求被控端强制关键帧
|
||||
stats.dec_fail.fetch_add(1, Ordering::Relaxed);
|
||||
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()));
|
||||
}
|
||||
|
||||
// 1) 渲染最新帧(只保留最新,丢弃积压);空帧=解码异常,请求关键帧
|
||||
loop {
|
||||
match frame_rx.try_recv() {
|
||||
Ok(raw) => {
|
||||
if raw.is_empty() {
|
||||
push(&msg_tx, ControlMsg::KeyFrameReq);
|
||||
} else if raw.len() == w * h * 3 {
|
||||
blit(&mut buffer, &raw);
|
||||
dirty = true;
|
||||
}
|
||||
// 1) 渲染最新帧(单槽 latest-wins:每迭代只取最新一帧,天然防积压)
|
||||
if let Ok(mut slot) = latest.lock() {
|
||||
if let Some((sw, sh, raw)) = slot.take() {
|
||||
if raw.is_empty() {
|
||||
push(&msg_tx, ControlMsg::KeyFrameReq);
|
||||
} else if sw > 0 && sh > 0 {
|
||||
blit(&mut buffer, w, h, &raw, sw as usize, sh as usize);
|
||||
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 缓冲
|
||||
fn blit(buffer: &mut [u32], raw: &[u8]) {
|
||||
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;
|
||||
/// RGB888 字节(openh264 解码输出)→ minifb 0RGB u32 缓冲。
|
||||
/// 支持源尺寸与窗口不同(解码器可能对齐到 16 倍数,如 1080→1088):
|
||||
/// 逐行拷贝重叠区域,源小于窗口时右侧/下方留黑。
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user