remote/viewer/src/main.rs

711 lines
27 KiB
Rust
Raw Normal View History

//! 主控端 Viewer:QUIC 连接被控端 → 解压帧 → minifb 渲染;采集键鼠上行。
//! HUD:右上角半透明白底参数面板(FPS / RTT / 端到端延迟 / 码率),延迟绿-黄-红分级。
mod keymap;
use anyhow::{bail, Result};
use common::{read_control, write_control, ControlMsg, InputEvent, MouseButton};
use minifb::{Key, MouseMode, Window, WindowOptions};
use openh264::formats::YUVSource;
use rustls::client::danger::{
HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier,
};
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use rustls::{DigitallySignedStruct, SignatureScheme};
use std::collections::HashSet;
use std::net::ToSocketAddrs;
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::{Duration, Instant};
/// MVP:跳过服务端证书校验(自签名)。正式版固定公钥/账号绑定。
#[derive(Debug)]
struct SkipVerify;
impl ServerCertVerifier for SkipVerify {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp_response: &[u8],
_now: UnixTime,
) -> Result<ServerCertVerified, rustls::Error> {
Ok(ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_certificate: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_certificate: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
vec![
SignatureScheme::RSA_PKCS1_SHA256,
SignatureScheme::ECDSA_NISTP256_SHA256,
SignatureScheme::ED25519,
SignatureScheme::RSA_PSS_SHA256,
]
}
}
fn parse_args() -> Result<(String, u16, String)> {
let mut addr = "127.0.0.1".to_string();
let mut port = 47800u16;
let mut code = "123456".to_string();
let a: Vec<String> = std::env::args().collect();
let mut i = 1;
while i < a.len() {
match a[i].as_str() {
"--addr" => addr = a[i + 1].clone(),
"--port" => port = a[i + 1].parse()?,
"--code" => code = a[i + 1].clone(),
_ => {}
}
i += 2;
}
Ok((addr, port, code))
}
static T0: OnceLock<Instant> = OnceLock::new();
fn now_ms() -> u64 {
T0.get().unwrap().elapsed().as_millis() as u64
}
/// 帧到达间隔环形缓冲(取 p95 用)
struct GapRing {
last: Option<Instant>,
ring: [u32; 64],
idx: usize,
filled: usize,
}
/// 会话统计(HUD 数据源,全部原子量,跨任务共享)
struct Stats {
/// 最近 RTT ms(0=未知)
rtt: AtomicU64,
/// 被控时钟 - 主控时钟 偏移估计 ms(EMA 平滑)
offset: AtomicI64,
offset_valid: AtomicBool,
/// 端到端延迟估计 ms(采集→网络→接收,EMA 平滑)
e2e: AtomicU64,
e2e_valid: AtomicBool,
/// 累计收帧数 / 字节数
frames: AtomicU64,
bytes: AtomicU64,
/// 抖动:相邻帧 |ΔE2E| 的 EMA(ms×16 定点存储避免浮点原子)
jitter_x16: AtomicU64,
prev_e2e: AtomicI64,
/// agent 发送侧(视频下行)QUIC 丢包/发包累计(最近一次 Pong)
agent_lost: AtomicU64,
agent_sent: AtomicU64,
/// H.264 解码失败计数(关键帧自愈验证指标)
dec_fail: AtomicU64,
/// 帧到达间隔环形缓冲
gaps: std::sync::Mutex<GapRing>,
}
impl Default for Stats {
fn default() -> Self {
Self {
rtt: AtomicU64::new(0),
offset: AtomicI64::new(0),
offset_valid: AtomicBool::new(false),
e2e: AtomicU64::new(0),
e2e_valid: AtomicBool::new(false),
frames: AtomicU64::new(0),
bytes: AtomicU64::new(0),
jitter_x16: AtomicU64::new(0),
prev_e2e: AtomicI64::new(-1),
agent_lost: AtomicU64::new(0),
agent_sent: AtomicU64::new(0),
dec_fail: AtomicU64::new(0),
gaps: std::sync::Mutex::new(GapRing {
last: None,
ring: [0; 64],
idx: 0,
filled: 0,
}),
}
}
}
impl Stats {
/// 记录一次帧到达:更新间隔环形缓冲与抖动 EMA
fn on_frame_arrival(&self, est: Option<i64>) {
let now = Instant::now();
if let Ok(mut g) = self.gaps.lock() {
if let Some(t) = g.last {
let gap = (now - t).as_millis() as u32;
let i = g.idx;
g.ring[i] = gap;
g.idx = (i + 1) % g.ring.len();
g.filled = (g.filled + 1).min(g.ring.len());
}
g.last = Some(now);
}
// 抖动 = |本帧E2E - 上帧E2E| 的 EMA(RFC3550 风格,÷16 衰减)
if let Some(e) = est {
let prev = self.prev_e2e.load(Ordering::Relaxed);
self.prev_e2e.store(e, Ordering::Relaxed);
if prev >= 0 {
let d = (e - prev).unsigned_abs() as u64;
let old = self.jitter_x16.load(Ordering::Relaxed);
let sm = if old == 0 { d * 16 } else { old + d * 16 / 16 - old / 16 };
self.jitter_x16.store(sm, Ordering::Relaxed);
}
}
}
/// 帧间隔 p95(ms),样本不足返回 None
fn gap_p95(&self) -> Option<u32> {
let g = self.gaps.lock().ok()?;
if g.filled < 8 {
return None;
}
let mut v: Vec<u32> = g.ring[..g.filled].to_vec();
v.sort_unstable();
v.get(v.len() * 95 / 100).copied()
}
}
fn main() -> Result<()> {
let (addr, port, code) = parse_args()?;
T0.set(Instant::now()).ok();
let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()?;
rt.block_on(run(addr, port, code))
}
async fn run(addr: String, port: u16, code: String) -> Result<()> {
rustls::crypto::ring::default_provider()
.install_default()
.ok();
let tls = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(SkipVerify))
.with_no_client_auth();
let qc: quinn::crypto::rustls::QuicClientConfig = Arc::new(tls).try_into()?;
// 接收窗口放大:视频是下行大流量方向,默认 512KB 流窗口会死锁吞吐
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));
let mut client_config = quinn::ClientConfig::new(Arc::new(qc));
client_config.transport_config(Arc::new(transport));
let mut endpoint = quinn::Endpoint::client("0.0.0.0:0".parse()?)?;
endpoint.set_default_client_config(client_config);
let sock_addr = (addr.as_str(), port)
.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow::anyhow!("resolve failed: {addr}"))?;
let conn = endpoint
.connect(sock_addr, "localhost")?
.await
.map_err(|e| anyhow::anyhow!("connect {addr}:{port} failed: {e}"))?;
println!("[viewer] connected to {addr}:{port}");
let (mut send, mut recv) = conn.open_bi().await?;
write_control(&mut send, &ControlMsg::Auth { code }).await?;
match read_control(&mut recv).await? {
ControlMsg::AuthOk { width, height } => {
println!("[viewer] auth ok, remote desktop {width}x{height}");
// 流 2:键鼠输入 + Ping/Pong 独立通道,不与视频流竞争
let (isend, irecv) = conn.open_bi().await?;
session(isend, irecv, recv, width, height)
}
ControlMsg::AuthFail(e) => bail!("auth failed: {e}"),
m => bail!("unexpected message: {m:?}"),
}
}
fn session(
send: quinn::SendStream,
mut irecv: quinn::RecvStream,
mut recv: quinn::RecvStream,
width: u32,
height: u32,
) -> Result<()> {
let (w, h) = (width as usize, height as usize);
let stats = Arc::new(Stats::default());
// 读任务(流 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)) => {
stats.bytes.fetch_add(payload.len() as u64, Ordering::Relaxed);
stats.frames.fetch_add(1, Ordering::Relaxed);
// 端到端延迟 = 接收时刻 - 时钟偏移 - 采集时刻(EMA 平滑)
let mut est = None;
if stats.offset_valid.load(Ordering::Relaxed) {
let e =
now_ms() as i64 - stats.offset.load(Ordering::Relaxed)
- hdr.ts_ms as i64;
if (0..5000).contains(&e) {
let old = stats.e2e.load(Ordering::Relaxed) as i64;
let sm = if stats.e2e_valid.load(Ordering::Relaxed) {
(old * 4 + e) / 5
} else {
e
};
stats.e2e.store(sm as u64, Ordering::Relaxed);
stats.e2e_valid.store(true, Ordering::Relaxed);
est = Some(sm);
}
}
stats.on_frame_arrival(est);
match decoder.decode(&payload) {
Ok(Some(yuv)) => {
let (dw, dh) = yuv.dimensions();
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);
// latest-wins:直接覆盖旧帧,无积压
if let Ok(mut slot) = latest.lock() {
*slot = Some((dw, dh, rgb));
}
}
Ok(None) => {}
Err(e) => {
// 解码异常:请求被控端强制关键帧
stats.dec_fail.fetch_add(1, Ordering::Relaxed);
eprintln!("[viewer] decode failed: {e}, request keyframe");
if let Ok(mut slot) = latest.lock() {
*slot = Some((0, 0, Vec::new()));
}
}
}
}
Err(e) => {
eprintln!("[viewer] stream closed: {e}");
break;
}
}
}
});
}
// 读任务(流 2):Pong → RTT 与时钟偏移估计
{
let stats = stats.clone();
tokio::spawn(async move {
loop {
match read_control(&mut irecv).await {
Ok(ControlMsg::Pong { t, agent_ts, lost, sent }) => {
let r = now_ms();
stats.rtt.store(r.saturating_sub(t), Ordering::Relaxed);
stats.agent_lost.store(lost, Ordering::Relaxed);
stats.agent_sent.store(sent, Ordering::Relaxed);
// agent_ts ≈ (t+r)/2 时刻的 agent 时钟读数 → 偏移 EMA
let est = agent_ts as i64 - ((t + r) / 2) as i64;
let old = stats.offset.load(Ordering::Relaxed);
let sm = if stats.offset_valid.load(Ordering::Relaxed) {
(old * 3 + est) / 4
} else {
est
};
stats.offset.store(sm, Ordering::Relaxed);
stats.offset_valid.store(true, Ordering::Relaxed);
}
Ok(_) => {}
Err(_) => break,
}
}
});
}
// 写任务(流 2):输入 / Ping
let (msg_tx, mut msg_rx) = tokio::sync::mpsc::unbounded_channel::<Vec<u8>>();
tokio::spawn(async move {
let mut send = send;
while let Some(b) = msg_rx.recv().await {
if send.write_all(&b).await.is_err() {
break;
}
}
let _ = send.finish();
});
let push = |tx: &tokio::sync::mpsc::UnboundedSender<Vec<u8>>, m: ControlMsg| {
let _ = tx.send(common::encode_control(&m));
};
// 入会立即请求 IDR:避免从 GOP 中间接入收到裸 P 帧解码失败导致启动黑屏
push(&msg_tx, ControlMsg::KeyFrameReq);
let mut window = Window::new(
"mvp-remote v0.7-LowLatency (Esc 退出)",
w,
h,
WindowOptions {
resize: false,
..Default::default()
},
)?;
let mut buffer = vec![0u32; w * h];
let mut prev_keys: HashSet<Key> = HashSet::new();
let mut prev_btns = [false; 3];
let mut last_pos: Option<(u16, u16)> = None;
let mut dirty = false;
let mut last_ping = Instant::now() - Duration::from_secs(1);
// HUD 每秒刷新的统计基线
let mut hud_last = Instant::now();
let mut hud_frames = 0u64;
let mut hud_bytes = 0u64;
let mut hud_fps = 0u64;
let mut hud_mbps = 0f64;
let mut hud_lost = 0u64;
let mut hud_sent = 0u64;
loop {
if !window.is_open() || window.is_key_down(Key::Escape) {
break;
}
// 0) 每 500ms 发一次 Ping(RTT/时钟偏移测量)
if last_ping.elapsed() >= Duration::from_millis(500) {
last_ping = Instant::now();
push(&msg_tx, ControlMsg::Ping(now_ms()));
}
// 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;
}
}
}
// 2) 键盘
let keys: HashSet<Key> = window.get_keys().into_iter().collect();
for k in keys.difference(&prev_keys) {
if let Some((sc, ext)) = keymap::map(*k) {
push(
&msg_tx,
ControlMsg::Input(InputEvent::KeyDown {
scancode: sc,
extended: ext,
}),
);
}
}
for k in prev_keys.difference(&keys) {
if let Some((sc, ext)) = keymap::map(*k) {
push(
&msg_tx,
ControlMsg::Input(InputEvent::KeyUp {
scancode: sc,
extended: ext,
}),
);
}
}
prev_keys = keys;
// 3) 鼠标移动(窗口坐标 → 虚拟桌面归一化 0..65535;位置变化才发送)
if let Some((x, y)) = window.get_mouse_pos(MouseMode::Discard) {
let nx = ((x / w as f32) * 65535.0).clamp(0.0, 65535.0) as u16;
let ny = ((y / h as f32) * 65535.0).clamp(0.0, 65535.0) as u16;
if last_pos != Some((nx, ny)) {
last_pos = Some((nx, ny));
push(&msg_tx, ControlMsg::Input(InputEvent::MouseMove { x: nx, y: ny }));
}
}
// 4) 鼠标按键(边沿触发)
for (i, (btn, mfb)) in BTN_MAP.iter().enumerate() {
let down = window.get_mouse_down(*mfb);
if down != prev_btns[i] {
prev_btns[i] = down;
push(
&msg_tx,
ControlMsg::Input(if down {
InputEvent::MouseDown(*btn)
} else {
InputEvent::MouseUp(*btn)
}),
);
}
}
// 5) 滚轮
if let Some((_, dy)) = window.get_scroll_wheel() {
if dy.abs() >= 1.0 {
push(
&msg_tx,
ControlMsg::Input(InputEvent::Wheel { dy: dy as i32 * 120 }),
);
}
}
// 6) HUD:指标每秒刷新一次;每秒至少强制呈现一次(视频冻结/黑屏时 HUD 依然可见)
let mut need_present = dirty;
let (hud_loss, hud_jit, hud_p95, hud_dec);
if hud_last.elapsed() >= Duration::from_secs(1) {
let f = stats.frames.load(Ordering::Relaxed);
let b = stats.bytes.load(Ordering::Relaxed);
hud_fps = f.saturating_sub(hud_frames);
hud_mbps = (b.saturating_sub(hud_bytes)) as f64 * 8.0 / 1e6;
hud_frames = f;
hud_bytes = b;
// 丢包率 = Δlost / Δsent(agent 发送方向,随 Pong 同步)
let l = stats.agent_lost.load(Ordering::Relaxed);
let s = stats.agent_sent.load(Ordering::Relaxed);
hud_loss = if s > hud_sent {
((l - hud_lost) as f64 / (s - hud_sent) as f64 * 100.0).min(99.9)
} else {
-1.0
};
hud_lost = l;
hud_sent = s;
hud_jit = stats.jitter_x16.load(Ordering::Relaxed) / 16;
hud_p95 = stats.gap_p95();
hud_dec = stats.dec_fail.load(Ordering::Relaxed);
hud_last = Instant::now();
need_present = true; // HUD 面板每秒至少重绘一次
} else {
hud_loss = f64::NAN;
hud_jit = 0;
hud_p95 = None;
hud_dec = 0;
}
if need_present {
let rtt = stats.rtt.load(Ordering::Relaxed);
let e2e = stats.e2e.load(Ordering::Relaxed);
let e2e_ok = stats.e2e_valid.load(Ordering::Relaxed);
let loss_str = if hud_loss.is_nan() {
"--".to_string()
} else if hud_loss < 0.0 {
"--".to_string()
} else {
format!("{hud_loss:.1}%")
};
let loss_c = if hud_loss.is_nan() || hud_loss < 0.0 {
C_GRAY
} else {
lat_color(hud_loss < 1.0, hud_loss < 5.0)
};
let jit_c = lat_color(hud_jit < 10, hud_jit < 30);
let p95_c = hud_p95
.map(|p| lat_color(p < 60, p < 150))
.unwrap_or(C_GRAY);
draw_hud(
&mut buffer,
w,
&[
("FPS", &format!("{hud_fps}"), lat_color(hud_fps >= 25, hud_fps >= 15)),
("RTT", &format!("{rtt}ms"), lat_color(rtt < 50 && rtt > 0, rtt < 100)),
(
"E2E",
&if e2e_ok { format!("{e2e}ms") } else { "--".into() },
lat_color(e2e < 100, e2e < 200),
),
("JIT", &format!("{hud_jit}ms"), jit_c),
("P95", &hud_p95.map(|p| format!("{p}ms")).unwrap_or("--".into()), p95_c),
("LOSS", &loss_str, loss_c),
("DECFAIL", &format!("{hud_dec}"), if hud_dec == 0 { C_GRAY } else { C_RED }),
("RATE", &format!("{:.0}MBP", hud_mbps), C_GRAY),
("RES", &format!("{w}x{h}"), C_GRAY),
],
);
window.update_with_buffer(&buffer, w, h)?;
dirty = false;
} else {
window.update();
std::thread::sleep(Duration::from_millis(4));
}
}
println!("[viewer] closed");
Ok(())
}
const BTN_MAP: [(MouseButton, minifb::MouseButton); 3] = [
(MouseButton::Left, minifb::MouseButton::Left),
(MouseButton::Right, minifb::MouseButton::Right),
(MouseButton::Middle, minifb::MouseButton::Middle),
];
// ---------- HUD ----------
const C_GREEN: u32 = 0x0A8F3C;
const C_AMBER: u32 = 0xC07000;
const C_RED: u32 = 0xC02222;
const C_GRAY: u32 = 0x3A3A3A;
fn lat_color(good: bool, mid: bool) -> u32 {
if good {
C_GREEN
} else if mid {
C_AMBER
} else {
C_RED
}
}
/// 5x7 点阵字体(列优先,bit0=顶行)。覆盖 HUD 用到的字符。
fn glyph(c: u8) -> [u8; 5] {
match c {
b'0' => [0x3E, 0x51, 0x49, 0x45, 0x3E],
b'1' => [0x00, 0x42, 0x7F, 0x40, 0x00],
b'2' => [0x42, 0x61, 0x51, 0x49, 0x46],
b'3' => [0x21, 0x41, 0x45, 0x4B, 0x31],
b'4' => [0x18, 0x14, 0x12, 0x7F, 0x10],
b'5' => [0x27, 0x45, 0x45, 0x45, 0x39],
b'6' => [0x3C, 0x4A, 0x49, 0x49, 0x30],
b'7' => [0x01, 0x71, 0x09, 0x05, 0x03],
b'8' => [0x36, 0x49, 0x49, 0x49, 0x36],
b'9' => [0x06, 0x49, 0x49, 0x29, 0x1E],
b'A' => [0x7E, 0x11, 0x11, 0x11, 0x7E],
b'B' => [0x7F, 0x49, 0x49, 0x49, 0x36],
b'C' => [0x3E, 0x41, 0x41, 0x41, 0x22],
b'D' => [0x7F, 0x41, 0x41, 0x22, 0x1C],
b'E' => [0x7F, 0x49, 0x49, 0x49, 0x41],
b'F' => [0x7F, 0x09, 0x09, 0x09, 0x01],
b'G' => [0x3E, 0x41, 0x49, 0x49, 0x7A],
b'H' => [0x7F, 0x08, 0x08, 0x08, 0x7F],
b'I' => [0x00, 0x41, 0x7F, 0x41, 0x00],
b'J' => [0x20, 0x40, 0x41, 0x3F, 0x01],
b'K' => [0x7F, 0x08, 0x14, 0x22, 0x41],
b'L' => [0x7F, 0x40, 0x40, 0x40, 0x40],
b'M' => [0x7F, 0x02, 0x0C, 0x02, 0x7F],
b'N' => [0x7F, 0x04, 0x08, 0x10, 0x7F],
b'O' => [0x3E, 0x41, 0x41, 0x41, 0x3E],
b'P' => [0x7F, 0x09, 0x09, 0x09, 0x06],
b'Q' => [0x3E, 0x41, 0x51, 0x21, 0x5E],
b'R' => [0x7F, 0x09, 0x19, 0x29, 0x46],
b'S' => [0x46, 0x49, 0x49, 0x49, 0x31],
b'T' => [0x01, 0x01, 0x7F, 0x01, 0x01],
b'U' => [0x3F, 0x40, 0x40, 0x40, 0x3F],
b'V' => [0x1F, 0x20, 0x40, 0x20, 0x1F],
b'W' => [0x7F, 0x20, 0x18, 0x20, 0x7F],
b'X' => [0x63, 0x14, 0x08, 0x14, 0x63],
b'Y' => [0x03, 0x04, 0x78, 0x04, 0x03],
b'Z' => [0x61, 0x51, 0x49, 0x45, 0x43],
b':' => [0x00, 0x36, 0x36, 0x00, 0x00],
b'.' => [0x00, 0x60, 0x60, 0x00, 0x00],
b'-' => [0x08, 0x08, 0x08, 0x08, 0x08],
b'%' => [0x13, 0x18, 0x04, 0x0A, 0x11],
b' ' => [0x00; 5],
_ => [0x00; 5],
}
}
/// 在 (x,y) 处以 scale 倍率画一行文字
fn draw_text(
buffer: &mut [u32],
w: usize,
mut x: usize,
y: usize,
s: &str,
color: u32,
scale: usize,
) {
let h = buffer.len() / w.max(1);
for ch in s.bytes() {
let g = glyph(ch.to_ascii_uppercase());
for (col, bits) in g.iter().enumerate() {
for row in 0..7 {
if bits & (1 << row) != 0 {
for sy in 0..scale {
for sx in 0..scale {
let px = x + col * scale + sx;
let py = y + row * scale + sy;
if px < w && py < h {
buffer[py * w + px] = color;
}
}
}
}
}
}
x += 6 * scale;
}
}
/// 右上角半透明白底 HUD:lines = [(标签, 值, 颜色)]
fn draw_hud(buffer: &mut [u32], w: usize, lines: &[(&str, &str, u32)]) {
const SCALE: usize = 2;
const PAD: usize = 10;
const MARGIN: usize = 12;
const LINE: usize = 8 * SCALE;
let h = buffer.len() / w.max(1);
let line_w = |l: &(&str, &str, u32)| (l.0.len() + 2 + l.1.len()) * 6 * SCALE;
let box_w = lines.iter().map(line_w).max().unwrap_or(0) + PAD * 2;
let box_h = lines.len() * LINE + PAD * 2;
let x0 = w.saturating_sub(box_w + MARGIN);
let y0 = MARGIN;
// 半透明白底:px' = px*0.35 + 255*0.65
for by in y0..(y0 + box_h).min(h) {
for bx in x0..(x0 + box_w).min(w) {
let p = &mut buffer[by * w + bx];
let v = *p;
let r = (v >> 16) & 0xFF;
let g = (v >> 8) & 0xFF;
let b = v & 0xFF;
let mix = |c: u32| c * 89 / 255 + 166;
*p = (mix(r) << 16) | (mix(g) << 8) | mix(b);
}
}
for (i, (label, value, color)) in lines.iter().enumerate() {
let mut x = x0 + PAD;
let y = y0 + PAD + i * LINE;
draw_text(buffer, w, x, y, label, C_GRAY, SCALE);
x += (label.len() + 2) * 6 * SCALE;
draw_text(buffer, w, x, y, value, *color, SCALE);
}
}
/// 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;
}
}
}