Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1837658c85 | ||
|
|
6e83bda8b8 | ||
|
|
8410a8de2a | ||
|
|
192513dd2a | ||
|
|
e778ff73cc | ||
|
|
e814cb64e7 | ||
|
|
9e50bc6237 | ||
|
|
703eb64fc0 | ||
|
|
4533968057 | ||
|
|
bae871c78c | ||
|
|
49f85db9f9 |
No files matched your search
Generated
-2
@@ -628,8 +628,6 @@ checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||
[[package]]
|
||||
name = "nvenc"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3227c3b09b1e042dab7d8d82cea77c0404e7d0c2bbb92e7e87cb319749cd20dc"
|
||||
dependencies = [
|
||||
"bitfields",
|
||||
"libloading",
|
||||
|
||||
+96
-25
@@ -11,8 +11,8 @@
|
||||
|
||||
use anyhow::Result;
|
||||
use common::{
|
||||
encode_video_frame, read_control, write_control, ControlMsg, InputEvent, MouseButton,
|
||||
VideoFrameHeader,
|
||||
encode_video_frame, h264_contains_idr, read_control, write_control, ControlMsg, InputEvent,
|
||||
MouseButton, VideoFrameHeader,
|
||||
};
|
||||
use openh264::encoder::{
|
||||
BitRate, Encoder, EncoderConfig, FrameRate, IntraFramePeriod, RateControlMode, UsageType,
|
||||
@@ -79,11 +79,14 @@ pub struct EncoderSettings {
|
||||
|
||||
impl Default for EncoderSettings {
|
||||
fn default() -> Self {
|
||||
// 对标 UU:屏幕内容实时档;关键帧周期 300 帧 ≈ 10s
|
||||
// 关键帧周期:以**时间**为准而非固定帧数,避免帧率变化后恢复窗口漂移。
|
||||
// 1 秒 @30fps = 30 帧。主恢复路径是 KeyFrameReq(协议 v2 后已确认生效),
|
||||
// 自动 IDR 只是兜底;此前 300 帧 @30fps 意味着兜底窗口长达 10 秒。
|
||||
let max_fps = 30.0f32;
|
||||
Self {
|
||||
bitrate_bps: 5_000_000,
|
||||
max_fps: 30.0,
|
||||
intra_period: 300,
|
||||
max_fps,
|
||||
intra_period: (max_fps as u32).max(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,23 +128,30 @@ enum LoopEnd {
|
||||
}
|
||||
|
||||
/// 编码一帧并推入下行通道。返回 false 表示通道已关闭(会话结束)。
|
||||
///
|
||||
/// `seq` 由调用方持有并自增:帧序号必须覆盖**每一次实际发送**(含静止桌面重发),
|
||||
/// 不能由编码器计数(编码器只知道编码了多少帧,不知道发了多少帧)。
|
||||
fn encode_send(
|
||||
encoder: &mut Encoder,
|
||||
raw: &[u8],
|
||||
(w, h): (u32, u32),
|
||||
ts_ms: u64,
|
||||
seq: u64,
|
||||
tx: &watch::Sender<Option<Vec<u8>>>,
|
||||
last_frame: &mut Option<Vec<u8>>,
|
||||
) -> bool {
|
||||
let header = VideoFrameHeader {
|
||||
width: w,
|
||||
height: h,
|
||||
ts_ms,
|
||||
};
|
||||
let yuv = YUVBuffer::from_bgra8_source(BgraSliceU8::new(raw, (w as usize, h as usize)));
|
||||
match encoder.encode(&yuv) {
|
||||
Ok(ebs) => {
|
||||
let frame = encode_video_frame(&header, &ebs.to_vec());
|
||||
let payload = ebs.to_vec();
|
||||
let header = VideoFrameHeader {
|
||||
width: w,
|
||||
height: h,
|
||||
ts_ms,
|
||||
seq,
|
||||
keyframe: h264_contains_idr(&payload),
|
||||
};
|
||||
let frame = encode_video_frame(&header, &payload);
|
||||
if tx.send(Some(frame.clone())).is_err() {
|
||||
return false;
|
||||
}
|
||||
@@ -178,6 +188,8 @@ fn capture_session(
|
||||
// 保留最近一帧裸像素:静止桌面收到关键帧请求时,仍能重编一个真关键帧
|
||||
let mut last_raw: Option<Vec<u8>> = None;
|
||||
let mut last_sent = Instant::now() - Duration::from_secs(10);
|
||||
// 帧序号:覆盖每一次实际发送(含静止桌面重发),供主控端做丢帧检测
|
||||
let mut seq: u64 = 0;
|
||||
|
||||
loop {
|
||||
let iter_start = Instant::now();
|
||||
@@ -189,9 +201,10 @@ fn capture_session(
|
||||
encoder.force_intra_frame();
|
||||
}
|
||||
let ts = start.elapsed().as_millis() as u64;
|
||||
if !encode_send(&mut encoder, &raw, (w, h), ts, tx, &mut last_frame) {
|
||||
if !encode_send(&mut encoder, &raw, (w, h), ts, seq, tx, &mut last_frame) {
|
||||
return LoopEnd::ChannelClosed; // 会话结束
|
||||
}
|
||||
seq = seq.wrapping_add(1);
|
||||
last_raw = Some(raw);
|
||||
last_sent = Instant::now();
|
||||
// 帧间隔节流到 max_fps
|
||||
@@ -206,24 +219,56 @@ fn capture_session(
|
||||
}
|
||||
},
|
||||
Ok(false) => {
|
||||
// 静止桌面:先看关键帧请求,再决定是否走 500ms 保活节流。
|
||||
//
|
||||
// ⚠️ 这里必须保证「消费了标志就一定发送」:先前写法是
|
||||
// `if force && last_raw.is_some() { ... }`,当 force 为真但
|
||||
// last_raw 还是 None(刚建会话、尚未抓到任何一帧)时,标志被
|
||||
// swap 消费掉却没有发送,解码端的这次请求就永久丢失。
|
||||
let force = keyreq.swap(false, Ordering::Relaxed);
|
||||
if force && last_raw.is_some() {
|
||||
let can_key = last_raw.is_some();
|
||||
if force && !can_key {
|
||||
// 还没有裸帧可重编:把请求还回去,让下一轮(拿到首帧后)继续生效
|
||||
keyreq.store(true, Ordering::Relaxed);
|
||||
eprintln!("[capture] KeyFrameReq 暂存:尚无裸帧可重编");
|
||||
std::thread::sleep(Duration::from_millis(8));
|
||||
} else if force {
|
||||
// 静止桌面下的关键帧恢复:画面没变不代表可以不给关键帧——
|
||||
// 解码端一旦出错,若只会收到旧 P 帧就永远恢复不了
|
||||
// 解码端一旦出错,若只会收到旧 P 帧就永远恢复不了。
|
||||
// 关键帧请求绕过 500ms 节流:它是错误恢复路径,延迟一次 IDR
|
||||
// 的代价是持续黑屏,远高于多发一帧的带宽成本。
|
||||
encoder.force_intra_frame();
|
||||
let raw = last_raw.as_ref().expect("上面已判断 is_some");
|
||||
let raw = last_raw.as_ref().expect("can_key 已保证");
|
||||
let ts = start.elapsed().as_millis() as u64;
|
||||
if !encode_send(&mut encoder, raw, (w, h), ts, tx, &mut last_frame) {
|
||||
if !encode_send(&mut encoder, raw, (w, h), ts, seq, tx, &mut last_frame) {
|
||||
return LoopEnd::ChannelClosed;
|
||||
}
|
||||
seq = seq.wrapping_add(1);
|
||||
last_sent = Instant::now();
|
||||
} else if last_sent.elapsed() > Duration::from_millis(500) && last_frame.is_some() {
|
||||
// 桌面静止:低频重发最后一帧(重复 P 帧,解码端画面不变)
|
||||
// 桌面静止:低频重发最后一帧(重复 P 帧,解码端画面不变)。
|
||||
// 帧头必须重建:直接 clone 旧帧会让 seq/ts_ms 停在上次发送时刻,
|
||||
// 破坏 seq 单调递增契约。
|
||||
let f = last_frame.as_ref().expect("上面已判断 is_some");
|
||||
if tx.send(Some(f.clone())).is_err() {
|
||||
return LoopEnd::ChannelClosed;
|
||||
match common::split_video_frame(f) {
|
||||
Ok((mut hdr, payload)) => {
|
||||
hdr.seq = seq;
|
||||
hdr.ts_ms = start.elapsed().as_millis() as u64;
|
||||
let frame = encode_video_frame(&hdr, payload);
|
||||
if tx.send(Some(frame)).is_err() {
|
||||
return LoopEnd::ChannelClosed;
|
||||
}
|
||||
seq = seq.wrapping_add(1);
|
||||
last_sent = Instant::now();
|
||||
}
|
||||
Err(e) => {
|
||||
// 自己产出的帧解析失败 = 编码/封装路径有 bug。
|
||||
// 丢弃缓存而不是每轮重复报错:静止桌面下这里会以 ~125Hz 触发。
|
||||
eprintln!("[capture] 静止重发时帧格式异常,丢弃缓存: {e}");
|
||||
last_frame = None;
|
||||
std::thread::sleep(Duration::from_millis(8));
|
||||
}
|
||||
}
|
||||
last_sent = Instant::now();
|
||||
} else {
|
||||
std::thread::sleep(Duration::from_millis(8));
|
||||
}
|
||||
@@ -318,20 +363,46 @@ where
|
||||
let (mut send, mut recv) = conn.accept_bi().await?;
|
||||
|
||||
let first = read_control(&mut recv).await?;
|
||||
let code_ok = matches!(&first, ControlMsg::Auth { code: c } if *c == cfg.code);
|
||||
if !code_ok {
|
||||
write_control(&mut send, &ControlMsg::AuthFail("bad code".into())).await?;
|
||||
// 先校验版本,再校验验证码:版本不一致时字段布局可能不同,验证码的解析结果不可信
|
||||
let auth_err: Option<String> = match &first {
|
||||
ControlMsg::Auth {
|
||||
code: c,
|
||||
proto_version,
|
||||
} => {
|
||||
if *proto_version != common::PROTO_VERSION {
|
||||
Some(format!(
|
||||
"protocol version mismatch: client={proto_version}, server={}",
|
||||
common::PROTO_VERSION
|
||||
))
|
||||
} else if *c != cfg.code {
|
||||
Some("bad code".into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
other => Some(format!("expected Auth, got {other:?}")),
|
||||
};
|
||||
if let Some(msg) = auth_err {
|
||||
write_control(&mut send, &ControlMsg::AuthFail(msg.clone())).await?;
|
||||
send.finish()?;
|
||||
// 必须等对端读到 AuthFail 并挂断再退出:直接 return 会让进程带走 QUIC
|
||||
// 缓冲区里尚未发出的数据,客户端只会看到 "connection lost" 而不是 AuthFail
|
||||
let _ = tokio::time::timeout(Duration::from_secs(5), conn.closed()).await;
|
||||
anyhow::bail!("auth failed");
|
||||
anyhow::bail!(msg);
|
||||
}
|
||||
|
||||
// 采集后端先建一次:既拿桌面尺寸回给主控端,也避免白建一个 duplication 只为读尺寸
|
||||
let first_cap = capture_factory()?;
|
||||
let (w, h) = first_cap.size();
|
||||
write_control(&mut send, &ControlMsg::AuthOk { width: w, height: h }).await?;
|
||||
write_control(
|
||||
&mut send,
|
||||
&ControlMsg::AuthOk {
|
||||
width: w,
|
||||
height: h,
|
||||
proto_version: common::PROTO_VERSION,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
println!("[agent] auth ok, desktop {w}x{h}");
|
||||
|
||||
// 流 2:键鼠输入 + 控制上行(与视频分离,避免被视频积压阻塞)
|
||||
|
||||
+4
-1
@@ -12,7 +12,10 @@ rustls.workspace = true
|
||||
rcgen.workspace = true
|
||||
lz4_flex.workspace = true
|
||||
openh264 = { version = "0.9.9", features = ["source"] }
|
||||
nvenc = "0.1.0"
|
||||
# 上游 nvenc 0.1.0 的 encode_picture 只设 picture_type=IDR、不置 FORCEIDR 标志,
|
||||
# 导致运行期 KeyFrameReq 被编码器静默忽略(实测输出 NAL type=1 而非 5)。
|
||||
# vendor 版本补上了该标志,见 vendor/nvenc/src/safe/encoder.rs 的「本项目补丁」注释。
|
||||
nvenc = { path = "../vendor/nvenc" }
|
||||
windows = { workspace = true, features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Direct3D",
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
//! 临时诊断工具:抓 agent 下发的码流,逐帧打印 NAL 结构并试解码。
|
||||
//!
|
||||
//! 目的:定位「首帧能解、之后连续 decode failed」的根因,不靠猜。
|
||||
//!
|
||||
//! 用法:
|
||||
//! cargo run --release -p agent --bin dump_stream -- --port 47805 --code t
|
||||
//! cargo run --release -p agent --bin dump_stream -- --port 47805 --code t --skip-at 2
|
||||
//!
|
||||
//! `--skip-at N`:模拟主控端的「积压跳帧」行为——第 N 帧读出但不喂给解码器,
|
||||
//! 用来验证「跳帧是否直接导致后续所有 P 帧解不出来」。
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use common::{read_control, read_video_frame, write_control, ControlMsg};
|
||||
use openh264::decoder::Decoder;
|
||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
|
||||
use rustls::pki_types::{CertificateDer, ServerName, UnixTime};
|
||||
use rustls::{DigitallySignedStruct, SignatureScheme};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SkipVerify;
|
||||
impl ServerCertVerifier for SkipVerify {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_e: &CertificateDer<'_>,
|
||||
_i: &[CertificateDer<'_>],
|
||||
_s: &ServerName<'_>,
|
||||
_o: &[u8],
|
||||
_n: UnixTime,
|
||||
) -> Result<ServerCertVerified, rustls::Error> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_m: &[u8],
|
||||
_c: &CertificateDer<'_>,
|
||||
_d: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_m: &[u8],
|
||||
_c: &CertificateDer<'_>,
|
||||
_d: &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,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
/// 扫 Annex-B 起始码,返回 NAL 类型列表(如 [7, 8, 5])
|
||||
fn nal_types(payload: &[u8]) -> Vec<u8> {
|
||||
let mut out = Vec::new();
|
||||
let mut i = 0usize;
|
||||
while i + 3 < payload.len() {
|
||||
let sc = if payload[i..].starts_with(&[0, 0, 0, 1]) {
|
||||
4
|
||||
} else if payload[i..].starts_with(&[0, 0, 1]) {
|
||||
3
|
||||
} else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
match payload.get(i + sc) {
|
||||
Some(&b) => {
|
||||
out.push(b & 0x1F);
|
||||
i += sc + 1;
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn describe(types: &[u8]) -> String {
|
||||
let has = |t: u8| types.contains(&t);
|
||||
let mut s = String::new();
|
||||
if has(7) {
|
||||
s.push_str("SPS ");
|
||||
}
|
||||
if has(8) {
|
||||
s.push_str("PPS ");
|
||||
}
|
||||
if has(5) {
|
||||
s.push_str("IDR ");
|
||||
}
|
||||
if has(1) {
|
||||
s.push_str("P ");
|
||||
}
|
||||
if has(6) {
|
||||
s.push_str("SEI ");
|
||||
}
|
||||
if s.is_empty() {
|
||||
s.push_str("(未知)");
|
||||
}
|
||||
format!("{}[{}]", s.trim(), types.len())
|
||||
}
|
||||
|
||||
fn arg(name: &str) -> Option<String> {
|
||||
let a: Vec<String> = std::env::args().collect();
|
||||
a.iter()
|
||||
.position(|x| x == name)
|
||||
.and_then(|i| a.get(i + 1))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
rustls::crypto::ring::default_provider()
|
||||
.install_default()
|
||||
.ok();
|
||||
|
||||
let port: u16 = arg("--port")
|
||||
.map(|s| s.parse())
|
||||
.transpose()?
|
||||
.unwrap_or(47800);
|
||||
let code = arg("--code").unwrap_or_else(|| "123456".into());
|
||||
let skip_at: Option<usize> = arg("--skip-at").map(|s| s.parse()).transpose()?;
|
||||
let total: usize = arg("--frames")
|
||||
.map(|s| s.parse())
|
||||
.transpose()?
|
||||
.unwrap_or(30);
|
||||
// --save <路径>:把**裸 H.264 码流**(各帧载荷按到达顺序直接拼接)
|
||||
// 写成 Annex-B 文件,供安卓 MediaCodec 等外部解码器验证兼容性。
|
||||
// 注意存的是载荷而非完整帧(不含自定义帧头),因为外部解码器只认标准 Annex-B。
|
||||
let save_path = arg("--save");
|
||||
|
||||
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()?;
|
||||
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 addr = ("127.0.0.1", port)
|
||||
.to_socket_addrs()?
|
||||
.next()
|
||||
.ok_or_else(|| anyhow::anyhow!("解析失败"))?;
|
||||
let conn = endpoint.connect(addr, "localhost")?.await?;
|
||||
println!("[dump] connected");
|
||||
|
||||
let (mut send, mut recv) = conn.open_bi().await?;
|
||||
write_control(
|
||||
&mut send,
|
||||
&ControlMsg::Auth {
|
||||
code,
|
||||
proto_version: common::PROTO_VERSION,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
match read_control(&mut recv).await? {
|
||||
ControlMsg::AuthOk {
|
||||
width,
|
||||
height,
|
||||
proto_version,
|
||||
} => println!("[dump] auth ok {width}x{height} proto={proto_version}"),
|
||||
other => bail!("认证失败: {other:?}"),
|
||||
}
|
||||
|
||||
let (mut isend, _irecv) = conn.open_bi().await?;
|
||||
|
||||
// 入会先请求一次 IDR,和 viewer 行为一致。
|
||||
// 注意:KeyFrameReq 必须走**流 2**(isend),agent 的读循环只监听流 2。
|
||||
write_control(&mut isend, &ControlMsg::KeyFrameReq).await?;
|
||||
|
||||
let mut decoder = Decoder::new()?;
|
||||
let mut sent_kfreq = false;
|
||||
// 累积裸码流,用于 --save
|
||||
let mut saved: Vec<u8> = Vec::new();
|
||||
|
||||
println!("\n序号 seq kf 载荷字节 NAL 结构 解码结果");
|
||||
println!("{}", "-".repeat(72));
|
||||
|
||||
for i in 0..total {
|
||||
let (hdr, payload) = match tokio::time::timeout(
|
||||
Duration::from_secs(10),
|
||||
read_video_frame(&mut recv),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r?,
|
||||
Err(_) => {
|
||||
println!("[{i}] 读帧超时(桌面可能完全静止)");
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
let types = nal_types(&payload);
|
||||
let is_idr = types.contains(&5);
|
||||
|
||||
// 需要在 save 里也保留这一帧时,先记录到缓冲
|
||||
if save_path.is_some() {
|
||||
saved.extend_from_slice(&payload);
|
||||
}
|
||||
|
||||
// 交叉校验:帧头 keyframe 标志必须与码流实际 NAL 类型一致。
|
||||
// 这条不一致意味着时间戳/序号契约或编码器标志有 bug,比画面花屏更难排查。
|
||||
if hdr.keyframe != is_idr {
|
||||
println!(
|
||||
" ⚠️ 帧头 keyframe={} 但码流实际 IDR={}(seq={})",
|
||||
hdr.keyframe, is_idr, hdr.seq
|
||||
);
|
||||
}
|
||||
|
||||
// 模拟主控端积压跳帧:读出但不解码
|
||||
if Some(i) == skip_at {
|
||||
println!(
|
||||
"[{i:2}] {:3} {:<5} {:8} {:<14} ⏭ 跳过解码(模拟积压跳帧)",
|
||||
hdr.seq,
|
||||
hdr.keyframe,
|
||||
payload.len(),
|
||||
describe(&types)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let res = match decoder.decode(&payload) {
|
||||
Ok(Some(_)) => "✅ 解出画面".to_string(),
|
||||
Ok(None) => "○ 无输出(缓冲中)".to_string(),
|
||||
Err(e) => format!("❌ {e}"),
|
||||
};
|
||||
|
||||
println!(
|
||||
"[{i:2}] {:3} {:<5} {:8} {:<14} {}",
|
||||
hdr.seq,
|
||||
hdr.keyframe,
|
||||
payload.len(),
|
||||
describe(&types),
|
||||
res
|
||||
);
|
||||
|
||||
// 和 viewer 一样:解码失败就请求关键帧
|
||||
if res.starts_with('❌') && !sent_kfreq {
|
||||
sent_kfreq = true;
|
||||
write_control(&mut isend, &ControlMsg::KeyFrameReq).await?;
|
||||
println!(" ↑ 已发 KeyFrameReq");
|
||||
}
|
||||
}
|
||||
|
||||
// 诊断:静止桌面下再请求一次关键帧,看是否真能拿到 IDR
|
||||
println!("\n[dump] 再发一次 KeyFrameReq,观察 10 帧…");
|
||||
write_control(&mut isend, &ControlMsg::KeyFrameReq).await?;
|
||||
for i in 0..10 {
|
||||
match tokio::time::timeout(Duration::from_secs(5), read_video_frame(&mut recv)).await {
|
||||
Ok(Ok((hdr, payload))) => {
|
||||
let types = nal_types(&payload);
|
||||
println!(
|
||||
"[k{i:2}] seq={:3} kf={:<5} {:6}B {}",
|
||||
hdr.seq,
|
||||
hdr.keyframe,
|
||||
payload.len(),
|
||||
describe(&types)
|
||||
);
|
||||
if types.contains(&5) {
|
||||
println!(" ↑ 收到 IDR");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!("[k{i:2}] 超时/流关闭");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出裸码流文件(Annex-B 拼接),供外部解码器验证
|
||||
if let Some(p) = save_path.as_ref() {
|
||||
std::fs::write(p, &saved)?;
|
||||
let idr_cnt = {
|
||||
// 粗略统计 IDR 数量:扫起始码后的 NAL 类型
|
||||
let mut n = 0;
|
||||
let mut i = 0usize;
|
||||
while i + 3 < saved.len() {
|
||||
let sc = if saved[i..].starts_with(&[0, 0, 0, 1]) {
|
||||
4
|
||||
} else if saved[i..].starts_with(&[0, 0, 1]) {
|
||||
3
|
||||
} else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
match saved.get(i + sc) {
|
||||
Some(&b) if b & 0x1F == 5 => {
|
||||
n += 1;
|
||||
i += sc + 1;
|
||||
}
|
||||
Some(_) => i += sc + 1,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
n
|
||||
};
|
||||
println!(
|
||||
"\n[dump] 已保存裸码流: {p}({} 字节,含 {idr_cnt} 个 IDR)",
|
||||
saved.len()
|
||||
);
|
||||
}
|
||||
|
||||
let _ = conn.close(0u32.into(), b"done");
|
||||
Ok(())
|
||||
}
|
||||
+119
-20
@@ -4,7 +4,7 @@
|
||||
//! 支持主控端 KeyFrameReq 强制关键帧(解码异常恢复)。
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use common::{encode_video_frame, VideoFrameHeader};
|
||||
use common::{encode_video_frame, h264_contains_idr, VideoFrameHeader};
|
||||
use nvenc::bitstream::BitStream;
|
||||
use nvenc::encoder::RegisteredResource;
|
||||
use nvenc::session::{InitParams, Session};
|
||||
@@ -39,6 +39,17 @@ use windows::Win32::Graphics::Dxgi::{
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// 周期 IDR 的帧间隔(兜底恢复点)。
|
||||
///
|
||||
/// 两个编码器共用同一常量,避免 NVENC 与软编的恢复窗口不一致——此前分别是
|
||||
/// 240 帧@60fps=4s 与 300 帧@60fps=5s,而 Linux 端又是 300 帧@30fps=10s。
|
||||
///
|
||||
/// 取值理由:**主恢复路径是 KeyFrameReq,不是自动 IDR**(协议 v2 后已确认请求
|
||||
/// 真正生效,见 vendor/nvenc 的 FORCEIDR 补丁)。自动 IDR 只用于兜底
|
||||
/// 「请求连续丢失」这种极端情况。60 帧 @60fps = 1 秒,既足够兜底,
|
||||
/// 又不会因过密的整帧内编码抬高码率。
|
||||
pub const INTRA_PERIOD_FRAMES: u32 = 60;
|
||||
|
||||
pub struct Duplication {
|
||||
pub device: ID3D11Device,
|
||||
context: ID3D11DeviceContext,
|
||||
@@ -201,8 +212,15 @@ 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 = 240; // 周期 IDR(60fps 下 4s):网络异常后的自愈恢复点,
|
||||
// 配合 KeyFrameReq 即时恢复,避免长时间冻结
|
||||
// 周期 IDR 是**兜底恢复点**,不是主恢复路径——主路径是 KeyFrameReq
|
||||
// (协议 v2 后已确认真正生效,见 vendor/nvenc 的 FORCEIDR 补丁)。
|
||||
//
|
||||
// 取值权衡:GOP 越短,丢帧后自动恢复越快,但每次 IDR 都是整帧内编码,
|
||||
// 码率开销显著(1080p 桌面 IDR 通常数倍于 P 帧)。
|
||||
// 60 帧 @60fps = 1s:既覆盖「KeyFrameReq 连续丢失」这种极端情况,
|
||||
// 又把兜底窗口压到 1 秒以内。原先 240 帧 = 4s,在请求已可靠的前提下
|
||||
// 只会让受害时长无谓地长 4 倍。
|
||||
config.preset_cfg.gop_len = INTRA_PERIOD_FRAMES;
|
||||
config.preset_cfg.frame_interval_p = 1; // 无 B 帧
|
||||
|
||||
let init = InitParams {
|
||||
@@ -312,7 +330,9 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>, congestion: Arc
|
||||
.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(
|
||||
INTRA_PERIOD_FRAMES,
|
||||
))
|
||||
.num_threads(4),
|
||||
) {
|
||||
Ok(e) => {
|
||||
@@ -327,29 +347,41 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>, congestion: Arc
|
||||
}
|
||||
};
|
||||
|
||||
let mut last_frame: Option<Vec<u8>> = None; // 软编静止重发
|
||||
// 最近一帧的**裸载荷**与关键帧标志:静止重发时要用它们重建帧头,
|
||||
// 不能直接重发整帧(那样 seq/ts_ms 会停在上次发送时刻)
|
||||
let mut last_payload: Option<Vec<u8>> = None;
|
||||
let mut last_kf = false;
|
||||
let mut last_sent = Instant::now() - Duration::from_secs(10);
|
||||
let min_frame = Duration::from_millis(16); // 60fps 上限
|
||||
let mut scratch: Vec<u8> = Vec::new(); // 软编:行距不连续时的复用缓冲
|
||||
// 帧序号:每发一帧 +1(含静止桌面的重复帧),供主控端做丢帧检测
|
||||
let mut seq: u64 = 0;
|
||||
// 编码耗时统计(每 120 帧输出一次均值,ms)
|
||||
let mut stat_frames = 0u64;
|
||||
let mut stat_convert_ms = 0u64;
|
||||
let mut stat_encode_ms = 0u64;
|
||||
let mut stat_skipped = 0u64; // 拥塞跳帧计数
|
||||
|
||||
// `$kf` 由调用点显式给出:NVENC 路径知道 `force_key`,软编路径同理。
|
||||
// 不要在这里猜——首帧和 KeyFrameReq 都会产生 IDR,猜错会让解码端错过重同步点。
|
||||
macro_rules! emit {
|
||||
($payload:expr) => {{
|
||||
($payload:expr, $kf:expr) => {{
|
||||
let header = VideoFrameHeader {
|
||||
width: dup.width,
|
||||
height: dup.height,
|
||||
ts_ms: start.elapsed().as_millis() as u64,
|
||||
seq,
|
||||
keyframe: $kf,
|
||||
};
|
||||
let frame = encode_video_frame(&header, &$payload);
|
||||
let payload_vec: Vec<u8> = $payload;
|
||||
let frame = encode_video_frame(&header, &payload_vec);
|
||||
if tx.send(Some(frame.clone())).is_err() {
|
||||
return; // 会话结束
|
||||
}
|
||||
last_frame = Some(frame);
|
||||
last_payload = Some(payload_vec);
|
||||
last_kf = $kf;
|
||||
last_sent = Instant::now();
|
||||
seq = seq.wrapping_add(1);
|
||||
stat_frames += 1;
|
||||
}};
|
||||
}
|
||||
@@ -358,10 +390,12 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>, congestion: Arc
|
||||
let iter_start = Instant::now();
|
||||
match dup.acquire(80) {
|
||||
Ok(true) => {
|
||||
// 主控端请求关键帧(解码异常恢复)
|
||||
let force_key = keyreq.swap(false, Ordering::Relaxed);
|
||||
// 拥塞反馈:发送侧排队深 → 跳过本帧(NVENC 少输入一帧,参考链连续;
|
||||
// 解码端收到的帧序 = 编码端实际输出序,无花屏风险)
|
||||
//
|
||||
// ⚠️ 必须放在消费 keyreq **之前**:先前顺序是「先 swap 再判断拥塞」,
|
||||
// 一旦本帧因拥塞被跳过,已消费的 KeyFrameReq 就随 continue 一起丢失,
|
||||
// 解码端等不到 IDR(与下方静止分支同源的缺陷)。
|
||||
if congestion.swap(false, Ordering::Relaxed) {
|
||||
stat_skipped += 1;
|
||||
let el = iter_start.elapsed();
|
||||
@@ -370,13 +404,19 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>, congestion: Arc
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 主控端请求关键帧(解码异常恢复)。走到这里一定会编码,
|
||||
// 因此可以安全地消费标志。
|
||||
let force_key = keyreq.swap(false, Ordering::Relaxed);
|
||||
match &mut encoder {
|
||||
EncoderKind::Nvenc(enc) => {
|
||||
let enc_start = Instant::now();
|
||||
let r = enc.encode(force_key, start.elapsed().as_millis() as u64);
|
||||
stat_encode_ms += enc_start.elapsed().as_millis() as u64;
|
||||
match r {
|
||||
Ok(payload) => emit!(payload),
|
||||
Ok(payload) => {
|
||||
let kf = h264_contains_idr(&payload);
|
||||
emit!(payload, kf)
|
||||
}
|
||||
Err(e) => eprintln!("[capture] nvenc encode failed: {e}"),
|
||||
}
|
||||
}
|
||||
@@ -410,7 +450,11 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>, congestion: Arc
|
||||
let r = enc.encode(&yuv);
|
||||
stat_encode_ms += enc_start.elapsed().as_millis() as u64;
|
||||
match r {
|
||||
Ok(ebs) => emit!(ebs.to_vec()),
|
||||
Ok(ebs) => {
|
||||
let payload = ebs.to_vec();
|
||||
let kf = h264_contains_idr(&payload);
|
||||
emit!(payload, kf)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[capture] encode failed: {e}");
|
||||
}
|
||||
@@ -442,25 +486,80 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>, congestion: Arc
|
||||
}
|
||||
Ok(false) => {
|
||||
// 桌面静止:低频重发(丢包后恢复 + 会话保活)。
|
||||
// 关键:KeyFrameReq 在桌面静止时也必须响应 —— 此前只在新帧路径
|
||||
// 消费该标志,桌面完全静止时解码端请求 IDR 永远得不到响应 → 黑屏。
|
||||
//
|
||||
// ⚠️ 这里曾有三个缺陷,全部围绕「KeyFrameReq 被静默丢弃」:
|
||||
// 1. `swap` 写在时间闸**外面**:请求若落在距上次发送 500ms 之内,
|
||||
// 标志已被消费但分支不进入,该次请求直接丢失(手册 §6.1 第 3 条)。
|
||||
// 2. 软编分支只重发旧码流、不看 force_key:永远吐不出 IDR(手册 §6.1 第 2 条)。
|
||||
// 3. NVENC 分支虽然把 force_key 传给了编码器,但受第 1 条限制。
|
||||
//
|
||||
// 现在的语义:**关键帧请求绕过 500ms 节流**——它是错误恢复路径,
|
||||
// 延迟一次 IDR 的代价是解码端持续黑屏,远高于多发一帧的带宽成本。
|
||||
let force_key = keyreq.swap(false, Ordering::Relaxed);
|
||||
if last_sent.elapsed() > Duration::from_millis(500) {
|
||||
let due = force_key || last_sent.elapsed() > Duration::from_millis(500);
|
||||
if due {
|
||||
match &mut encoder {
|
||||
EncoderKind::Nvenc(enc) => {
|
||||
// 重编当前 GPU 帧(内容未变,输出极小;force_key 时为 IDR)
|
||||
let r = enc.encode(force_key, start.elapsed().as_millis() as u64);
|
||||
match r {
|
||||
Ok(payload) => emit!(payload),
|
||||
Ok(payload) => {
|
||||
let kf = h264_contains_idr(&payload);
|
||||
emit!(payload, kf)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("[capture] nvenc idle encode failed: {e}")
|
||||
}
|
||||
}
|
||||
}
|
||||
EncoderKind::Soft(_) => {
|
||||
// 软编:直接重发最后一帧码流副本
|
||||
if let Some(f) = last_frame.as_ref() {
|
||||
let _ = tx.send(Some(f.clone()));
|
||||
EncoderKind::Soft(enc) => {
|
||||
// 软编:静止桌面没有新像素可编,只能重发最近一帧。
|
||||
// 但 force_key 时必须真的重编出 IDR —— 重发旧 P 帧对
|
||||
// 已经解不动的解码端毫无帮助。
|
||||
//
|
||||
// 裸像素来源:staging 纹理里仍是上一帧内容(DXGI 只在
|
||||
// 有新帧时覆盖),重新 Map 一次即可,不必每帧额外拷 14MB。
|
||||
if force_key {
|
||||
enc.force_intra_frame();
|
||||
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,
|
||||
))
|
||||
}
|
||||
});
|
||||
match conv {
|
||||
Ok(yuv) => match enc.encode(&yuv) {
|
||||
Ok(ebs) => {
|
||||
let payload = ebs.to_vec();
|
||||
let kf = h264_contains_idr(&payload);
|
||||
emit!(payload, kf)
|
||||
}
|
||||
Err(e) => eprintln!(
|
||||
"[capture] idle keyframe encode failed: {e}"
|
||||
),
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("[capture] idle keyframe convert failed: {e}")
|
||||
}
|
||||
}
|
||||
} else if let Some(p) = last_payload.as_ref() {
|
||||
// 非关键帧的保活重发:复用载荷但重建帧头(seq 必须递增)
|
||||
let kf = last_kf;
|
||||
emit!(p.clone(), kf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+32
-6
@@ -69,16 +69,42 @@ async fn main() -> Result<()> {
|
||||
// 流 1:认证 + 视频下行
|
||||
let (mut send, mut recv) = conn.accept_bi().await?;
|
||||
|
||||
// 认证
|
||||
// 认证:先校验版本,再校验验证码。
|
||||
// 顺序刻意如此——版本不一致时字段布局可能不同,验证码的解析结果不可信。
|
||||
let first = read_control(&mut recv).await?;
|
||||
let code_ok = matches!(&first, ControlMsg::Auth { code: c } if *c == code);
|
||||
if !code_ok {
|
||||
write_control(&mut send, &ControlMsg::AuthFail("bad code".into())).await?;
|
||||
let auth_err: Option<String> = match &first {
|
||||
ControlMsg::Auth {
|
||||
code: c,
|
||||
proto_version,
|
||||
} => {
|
||||
if *proto_version != common::PROTO_VERSION {
|
||||
Some(format!(
|
||||
"protocol version mismatch: client={proto_version}, server={}",
|
||||
common::PROTO_VERSION
|
||||
))
|
||||
} else if *c != code {
|
||||
Some("bad code".into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
other => Some(format!("expected Auth, got {other:?}")),
|
||||
};
|
||||
if let Some(msg) = auth_err {
|
||||
write_control(&mut send, &ControlMsg::AuthFail(msg.clone())).await?;
|
||||
send.finish()?;
|
||||
anyhow::bail!("auth failed");
|
||||
anyhow::bail!(msg);
|
||||
}
|
||||
let (w, h) = capture::desktop_size()?;
|
||||
write_control(&mut send, &ControlMsg::AuthOk { width: w, height: h }).await?;
|
||||
write_control(
|
||||
&mut send,
|
||||
&ControlMsg::AuthOk {
|
||||
width: w,
|
||||
height: h,
|
||||
proto_version: common::PROTO_VERSION,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
println!("[agent] auth ok, desktop {w}x{h}");
|
||||
|
||||
// 流 2:键鼠输入 + 控制上行(与视频分离,避免被视频积压阻塞)
|
||||
|
||||
+314
-7
@@ -7,12 +7,29 @@ use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
/// 媒体/控制协议版本。
|
||||
///
|
||||
/// **改动规则**:任何对 `ControlMsg` 或 `VideoFrameHeader` 字段的增删改都必须递增此值。
|
||||
/// 理由:线格式是 bincode(按位置编码,无字段名),新增字段会让旧端解析失败或错位,
|
||||
/// 且失败方式可能是静默的错误数据而非明确报错。递增后由 `Auth` 握手显式拒绝旧端,
|
||||
/// 把"连上了但行为诡异"变成"连不上且提示升级"。
|
||||
///
|
||||
/// - v1:初版(无版本字段,隐式)
|
||||
/// - v2:`Auth`/`AuthOk` 携带版本;帧头增加 `seq`/`keyframe`
|
||||
pub const PROTO_VERSION: u16 = 2;
|
||||
|
||||
/// 控制通道消息(双向)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ControlMsg {
|
||||
/// 主控端认证(临时验证码,MVP 明文走 QUIC-TLS 加密通道)
|
||||
Auth { code: String },
|
||||
AuthOk { width: u32, height: u32 },
|
||||
/// 主控端认证(临时验证码,MVP 明文走 QUIC-TLS 加密通道)。
|
||||
/// `proto_version` 必须等于 [`PROTO_VERSION`],否则被控端回 `AuthFail` 并拒绝。
|
||||
Auth { code: String, proto_version: u16 },
|
||||
/// 认证通过。`proto_version` 回显被控端的版本,供主控端确认双方一致。
|
||||
AuthOk {
|
||||
width: u32,
|
||||
height: u32,
|
||||
proto_version: u16,
|
||||
},
|
||||
AuthFail(String),
|
||||
KeyFrameReq,
|
||||
Ping(u64),
|
||||
@@ -42,12 +59,22 @@ pub enum InputEvent {
|
||||
KeyUp { scancode: u16, extended: bool },
|
||||
}
|
||||
|
||||
/// 视频帧头(载荷为 LZ4 压缩的 BGRA 像素,按行紧凑排列)
|
||||
/// 视频帧头。载荷为 H.264 Annex-B 码流(含 00 00 00 01 起始码)。
|
||||
///
|
||||
/// 字段语义:
|
||||
/// - `width` / `height`:被控端桌面尺寸(逻辑尺寸,非编码对齐后的尺寸)
|
||||
/// - `ts_ms`:被控端采集时刻(被控时钟),配合 Ping/Pong 估计的时钟偏移算端到端延迟
|
||||
/// - `seq`:帧序号,从 0 单调递增,每发一帧 +1(含静止桌面的重复帧)。
|
||||
/// 用于丢帧检测与弱网统计;注意它统计的是"发送序",不是"显示序"
|
||||
/// - `keyframe`:本帧是否 IDR。解码端(尤其 MediaCodec)需要它决定是否重置解码器,
|
||||
/// 不必再自己扫 NAL 起始码判断
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VideoFrameHeader {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub ts_ms: u64,
|
||||
pub seq: u64,
|
||||
pub keyframe: bool,
|
||||
}
|
||||
|
||||
async fn read_u32le<R: AsyncReadExt + Unpin>(r: &mut R) -> Result<u32> {
|
||||
@@ -81,7 +108,9 @@ pub async fn read_control<R: AsyncReadExt + Unpin>(r: &mut R) -> Result<ControlM
|
||||
Ok(bincode::deserialize(&buf)?)
|
||||
}
|
||||
|
||||
/// 编码视频帧:[总长 u32][头长 u32][bincode 头][LZ4 载荷]
|
||||
/// 编码视频帧:[总长 u32][头长 u32][bincode 头][H.264 载荷]
|
||||
///
|
||||
/// `总长` 不含自身这 4 字节,等于 `4 + 头长 + 载荷长`。
|
||||
pub fn encode_video_frame(header: &VideoFrameHeader, payload: &[u8]) -> Vec<u8> {
|
||||
let hb = bincode::serialize(header).expect("serialize header");
|
||||
let total = 4 + hb.len() + payload.len();
|
||||
@@ -93,14 +122,292 @@ pub fn encode_video_frame(header: &VideoFrameHeader, payload: &[u8]) -> Vec<u8>
|
||||
out
|
||||
}
|
||||
|
||||
/// 单帧总长上限(256 MiB)。超过即视为协议错误或恶意输入。
|
||||
const MAX_VIDEO_FRAME: usize = 256 << 20;
|
||||
|
||||
/// 判断一段 Annex-B H.264 码流里是否含 IDR 片(NAL 类型 5)。
|
||||
///
|
||||
/// 用途:填充 [`VideoFrameHeader::keyframe`]。
|
||||
/// 为什么不问编码器要这个标志——NVENC 与 openh264 的 API 都能间接推出,
|
||||
/// 但两者规则不同(首帧强制、`force_intra_frame`、GOP 自动 IDR 交织),
|
||||
/// 而**实际输出**是唯一可信来源。直接看码流,两种编码器一套逻辑。
|
||||
///
|
||||
/// 只识别 3/4 字节起始码;`start-code emulation prevention` 由编码器保证,
|
||||
/// 因此不会在 SPS/PPS 载荷里误判出起始码。
|
||||
pub fn h264_contains_idr(payload: &[u8]) -> bool {
|
||||
let mut i = 0usize;
|
||||
while i + 3 < payload.len() {
|
||||
let sc = if payload[i..].starts_with(&[0, 0, 0, 1]) {
|
||||
4
|
||||
} else if payload[i..].starts_with(&[0, 0, 1]) {
|
||||
3
|
||||
} else {
|
||||
i += 1;
|
||||
continue;
|
||||
};
|
||||
match payload.get(i + sc) {
|
||||
// nal_unit_type 占首字节低 5 位
|
||||
Some(&b) if b & 0x1F == 5 => return true,
|
||||
Some(_) => i += sc + 1,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// 读一个视频帧。
|
||||
///
|
||||
/// 对端数据一律视为不可信输入:`total` 与 `hlen` 都必须做**上下界**校验,
|
||||
/// 否则构造一个 `total < 4` 的帧就能让接收端在切片处 panic(进程级拒绝服务)。
|
||||
pub async fn read_video_frame<R: AsyncReadExt + Unpin>(
|
||||
r: &mut R,
|
||||
) -> Result<(VideoFrameHeader, Vec<u8>)> {
|
||||
let total = read_u32le(r).await? as usize;
|
||||
anyhow::ensure!(total < 256 << 20, "video frame too large: {total}");
|
||||
// 下界:至少要容得下「头长字段(4) + 一个非空的 bincode 头」。
|
||||
// 上界:防止对端声明超大长度导致巨量分配。
|
||||
anyhow::ensure!(
|
||||
(4..MAX_VIDEO_FRAME).contains(&total),
|
||||
"video frame length out of range: {total}"
|
||||
);
|
||||
|
||||
let mut buf = vec![0u8; total];
|
||||
r.read_exact(&mut buf).await?;
|
||||
let hlen = u32::from_le_bytes(buf[0..4].try_into().unwrap()) as usize;
|
||||
|
||||
let hlen = u32::from_le_bytes(buf[0..4].try_into().expect("已校验 total >= 4")) as usize;
|
||||
// hlen 必须落在 [1, total-4] 内:0 无法反序列化,超出则切片越界
|
||||
anyhow::ensure!(
|
||||
hlen > 0 && hlen <= total - 4,
|
||||
"video header length invalid: hlen={hlen}, total={total}"
|
||||
);
|
||||
|
||||
let header: VideoFrameHeader = bincode::deserialize(&buf[4..4 + hlen])?;
|
||||
Ok((header, buf[4 + hlen..].to_vec()))
|
||||
}
|
||||
|
||||
/// 把一个已编码的完整帧拆回 `(帧头, 载荷)`。
|
||||
///
|
||||
/// 用途:静止桌面重发时,被控端需要复用上一帧的**载荷**但重建**帧头**
|
||||
/// (`seq` 必须递增、`ts_ms` 必须是本次发送时刻)。直接 `clone` 整个帧会让
|
||||
/// 这两个字段停在上一次发送的时刻,破坏 `seq` 单调递增契约。
|
||||
///
|
||||
/// 只接受本模块 [`encode_video_frame`] 产出的字节;格式不符返回 `Err`。
|
||||
pub fn split_video_frame(frame: &[u8]) -> Result<(VideoFrameHeader, &[u8])> {
|
||||
anyhow::ensure!(frame.len() >= 8, "frame too short: {}", frame.len());
|
||||
let hlen = u32::from_le_bytes(frame[4..8].try_into().expect("已校验 len >= 8")) as usize;
|
||||
anyhow::ensure!(
|
||||
hlen > 0 && hlen <= frame.len() - 8,
|
||||
"header length invalid: hlen={hlen}, len={}",
|
||||
frame.len()
|
||||
);
|
||||
let header: VideoFrameHeader = bincode::deserialize(&frame[8..8 + hlen])?;
|
||||
Ok((header, &frame[8 + hlen..]))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tokio::io::duplex;
|
||||
|
||||
fn hdr(seq: u64, keyframe: bool) -> VideoFrameHeader {
|
||||
VideoFrameHeader {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
ts_ms: 12345,
|
||||
seq,
|
||||
keyframe,
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 控制消息 ----
|
||||
|
||||
#[test]
|
||||
fn control_roundtrip_preserves_version() {
|
||||
let m = ControlMsg::Auth {
|
||||
code: "123456".into(),
|
||||
proto_version: PROTO_VERSION,
|
||||
};
|
||||
let bytes = encode_control(&m);
|
||||
// 长度前缀 + 载荷
|
||||
let len = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
|
||||
assert_eq!(len, bytes.len() - 4);
|
||||
let back: ControlMsg = bincode::deserialize(&bytes[4..]).unwrap();
|
||||
match back {
|
||||
ControlMsg::Auth { code, proto_version } => {
|
||||
assert_eq!(code, "123456");
|
||||
assert_eq!(proto_version, PROTO_VERSION);
|
||||
}
|
||||
other => panic!("期望 Auth,实际 {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn control_write_read_roundtrip() {
|
||||
let (mut a, mut b) = duplex(64 * 1024);
|
||||
let msgs = [
|
||||
ControlMsg::Auth {
|
||||
code: "x".into(),
|
||||
proto_version: PROTO_VERSION,
|
||||
},
|
||||
ControlMsg::AuthOk {
|
||||
width: 800,
|
||||
height: 600,
|
||||
proto_version: PROTO_VERSION,
|
||||
},
|
||||
ControlMsg::KeyFrameReq,
|
||||
ControlMsg::Ping(42),
|
||||
ControlMsg::Pong {
|
||||
t: 1,
|
||||
agent_ts: 2,
|
||||
lost: 3,
|
||||
sent: 4,
|
||||
},
|
||||
];
|
||||
for m in &msgs {
|
||||
write_control(&mut a, m).await.unwrap();
|
||||
let got = read_control(&mut b).await.unwrap();
|
||||
assert_eq!(format!("{got:?}"), format!("{m:?}"));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 视频帧头 ----
|
||||
|
||||
#[test]
|
||||
fn video_header_roundtrip_keeps_seq_and_keyframe() {
|
||||
for (seq, kf) in [(0u64, true), (1, false), (u64::MAX, false)] {
|
||||
let h = hdr(seq, kf);
|
||||
let payload = b"\x00\x00\x00\x01\x65fake-nal";
|
||||
let bytes = encode_video_frame(&h, payload);
|
||||
|
||||
let total = u32::from_le_bytes(bytes[0..4].try_into().unwrap()) as usize;
|
||||
assert_eq!(total, bytes.len() - 4, "总长字段应不含自身");
|
||||
|
||||
let hlen = u32::from_le_bytes(bytes[4..8].try_into().unwrap()) as usize;
|
||||
let back: VideoFrameHeader = bincode::deserialize(&bytes[8..8 + hlen]).unwrap();
|
||||
assert_eq!(back.seq, seq);
|
||||
assert_eq!(back.keyframe, kf);
|
||||
assert_eq!(&bytes[8 + hlen..], payload);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn video_frame_write_read_roundtrip() {
|
||||
let (mut a, mut b) = duplex(256 * 1024);
|
||||
let h = hdr(7, true);
|
||||
let payload = vec![0xABu8; 4096];
|
||||
let bytes = encode_video_frame(&h, &payload);
|
||||
tokio::io::AsyncWriteExt::write_all(&mut a, &bytes).await.unwrap();
|
||||
|
||||
let (h2, p2) = read_video_frame(&mut b).await.unwrap();
|
||||
assert_eq!(h2.seq, 7);
|
||||
assert!(h2.keyframe);
|
||||
assert_eq!((h2.width, h2.height), (1920, 1080));
|
||||
assert_eq!(p2, payload);
|
||||
}
|
||||
|
||||
// ---- 不可信输入(P3 的核心回归) ----
|
||||
|
||||
/// `total < 4` 必须报错而不是 panic:旧实现在这里 `buf[0..4]` 会越界 panic。
|
||||
#[tokio::test]
|
||||
async fn rejects_total_below_minimum() {
|
||||
for total in [0u32, 1, 3] {
|
||||
let (mut a, mut b) = duplex(64);
|
||||
tokio::io::AsyncWriteExt::write_all(&mut a, &total.to_le_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
let err = read_video_frame(&mut b).await.unwrap_err().to_string();
|
||||
assert!(
|
||||
err.contains("out of range"),
|
||||
"total={total} 应被拒绝,实际错误: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `total == 4` 但没有头数据:hlen 读出来是 0,必须拒绝。
|
||||
#[tokio::test]
|
||||
async fn rejects_zero_header_length() {
|
||||
let (mut a, mut b) = duplex(64);
|
||||
let mut bytes = 4u32.to_le_bytes().to_vec();
|
||||
bytes.extend_from_slice(&0u32.to_le_bytes());
|
||||
tokio::io::AsyncWriteExt::write_all(&mut a, &bytes)
|
||||
.await
|
||||
.unwrap();
|
||||
let err = read_video_frame(&mut b).await.unwrap_err().to_string();
|
||||
assert!(err.contains("header length invalid"), "实际错误: {err}");
|
||||
}
|
||||
|
||||
/// hlen 声明超过 total-4:旧实现会切片越界 panic。
|
||||
#[tokio::test]
|
||||
async fn rejects_header_length_beyond_frame() {
|
||||
let (mut a, mut b) = duplex(64);
|
||||
// total = 8 → body 必须正好 8 字节(hlen 字段 4 + 4 字节填充)
|
||||
let mut bytes = 8u32.to_le_bytes().to_vec();
|
||||
bytes.extend_from_slice(&999u32.to_le_bytes()); // hlen = 999 > total-4 = 4
|
||||
bytes.extend_from_slice(&[0u8; 4]);
|
||||
tokio::io::AsyncWriteExt::write_all(&mut a, &bytes)
|
||||
.await
|
||||
.unwrap();
|
||||
let err = read_video_frame(&mut b).await.unwrap_err().to_string();
|
||||
assert!(err.contains("header length invalid"), "实际错误: {err}");
|
||||
}
|
||||
|
||||
/// 超过 256 MiB 上限必须在**分配前**拒绝,不能先 vec! 再检查。
|
||||
#[tokio::test]
|
||||
async fn rejects_oversized_frame_without_allocating() {
|
||||
let (mut a, mut b) = duplex(64);
|
||||
let total = (MAX_VIDEO_FRAME as u32) + 1;
|
||||
tokio::io::AsyncWriteExt::write_all(&mut a, &total.to_le_bytes())
|
||||
.await
|
||||
.unwrap();
|
||||
let err = read_video_frame(&mut b).await.unwrap_err().to_string();
|
||||
assert!(err.contains("out of range"), "实际错误: {err}");
|
||||
}
|
||||
|
||||
/// 头部字节损坏(非合法 bincode)应返回 Err 而非 panic。
|
||||
#[tokio::test]
|
||||
async fn rejects_corrupt_header_bytes() {
|
||||
let (mut a, mut b) = duplex(64);
|
||||
let garbage = [0xFFu8; 8];
|
||||
let total = (4 + garbage.len()) as u32;
|
||||
let mut bytes = total.to_le_bytes().to_vec();
|
||||
bytes.extend_from_slice(&(garbage.len() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&garbage);
|
||||
tokio::io::AsyncWriteExt::write_all(&mut a, &bytes)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(read_video_frame(&mut b).await.is_err());
|
||||
}
|
||||
|
||||
// ---- IDR 识别(P2 的 keyframe 标志来源) ----
|
||||
|
||||
#[test]
|
||||
fn detects_idr_in_annex_b() {
|
||||
// SPS(7) + PPS(8) + IDR 片(5),4 字节起始码
|
||||
let idr = [
|
||||
0, 0, 0, 1, 0x67, 0xAA, // SPS
|
||||
0, 0, 0, 1, 0x68, 0xBB, // PPS
|
||||
0, 0, 0, 1, 0x65, 0xCC, // IDR
|
||||
];
|
||||
assert!(h264_contains_idr(&idr));
|
||||
|
||||
// 3 字节起始码也应识别
|
||||
let idr3 = [0, 0, 1, 0x65, 0xCC];
|
||||
assert!(h264_contains_idr(&idr3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_idr_payloads() {
|
||||
// 纯 P 片(NAL 类型 1)
|
||||
let p_frame = [0, 0, 0, 1, 0x41, 0xDD];
|
||||
assert!(!h264_contains_idr(&p_frame));
|
||||
|
||||
// 只有 SPS/PPS,没有片(关键帧边界可能落在下一个包,此时不应谎报 keyframe)
|
||||
let params_only = [
|
||||
0, 0, 0, 1, 0x67, 0xAA,
|
||||
0, 0, 0, 1, 0x68, 0xBB,
|
||||
];
|
||||
assert!(!h264_contains_idr(¶ms_only));
|
||||
|
||||
assert!(!h264_contains_idr(&[]));
|
||||
assert!(!h264_contains_idr(&[0, 0, 1])); // 起始码后无 NAL 字节
|
||||
}
|
||||
}
|
||||
@@ -242,8 +242,10 @@ mod tests {
|
||||
#[test]
|
||||
fn coordinate_scaling_covers_full_range() {
|
||||
assert_eq!(scale(0, 1920), 0);
|
||||
// 归一化上界必须映射到 extent-1,否则指针永远到不了屏幕最右/最下
|
||||
assert_eq!(scale(65535, 1920), 1919);
|
||||
assert_eq!(scale(32768, 1920), 960);
|
||||
// 中点:32768 × 1919 ÷ 65535 = 959.49…,整数除法向下取整 → 959
|
||||
assert_eq!(scale(32768, 1920), 959);
|
||||
assert_eq!(scale(12345, 0), 0); // 退化尺寸不 panic
|
||||
}
|
||||
}
|
||||
@@ -289,11 +289,17 @@ async fn end_to_end_capture_encode_transport_input() -> Result<()> {
|
||||
&mut send,
|
||||
&ControlMsg::Auth {
|
||||
code: "smoke-code".into(),
|
||||
proto_version: common::PROTO_VERSION,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
match read_control(&mut recv).await? {
|
||||
ControlMsg::AuthOk { width, height } => {
|
||||
ControlMsg::AuthOk {
|
||||
width,
|
||||
height,
|
||||
proto_version,
|
||||
} => {
|
||||
assert_eq!(proto_version, common::PROTO_VERSION, "被控端协议版本不符");
|
||||
assert_eq!(
|
||||
(width, height),
|
||||
(sw, sh),
|
||||
@@ -429,6 +435,7 @@ async fn rejects_wrong_verification_code() -> Result<()> {
|
||||
&mut send,
|
||||
&ControlMsg::Auth {
|
||||
code: "wrong-code".into(),
|
||||
proto_version: common::PROTO_VERSION,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -43,6 +43,9 @@ if [ "$ready" -ne 1 ]; then
|
||||
fi
|
||||
echo "[smoke] X server 就绪:$(xdpyinfo -display "$DISPLAY_NUM" | grep -E 'dimensions' | head -1)"
|
||||
|
||||
# 两个测试共用同一块虚拟屏与指针,必须串行
|
||||
# 先跑纯逻辑单测(键码映射 / 坐标换算),不需要 X server
|
||||
cargo test -p linux-agent --bins
|
||||
|
||||
# 再跑端到端用例:两个用例共用同一块虚拟屏与指针,必须串行
|
||||
cargo test -p linux-agent --test smoke -- --nocapture --test-threads=1
|
||||
echo "[smoke] 通过"
|
||||
@@ -0,0 +1,2 @@
|
||||
/target
|
||||
/.vscode
|
||||
Vendored
+63
@@ -0,0 +1,63 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
rust-version = "1.85.1"
|
||||
name = "nvenc"
|
||||
version = "0.1.0"
|
||||
authors = ["AlsoSylv"]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "Wrapper for the nvidia NVENC API"
|
||||
readme = "README.md"
|
||||
keywords = [
|
||||
"nvidia",
|
||||
"graphics",
|
||||
"media",
|
||||
"video",
|
||||
"ffi",
|
||||
]
|
||||
categories = [
|
||||
"multimedia",
|
||||
"external-ffi-bindings",
|
||||
]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/AlsoSylv/nvenc"
|
||||
|
||||
[lib]
|
||||
name = "nvenc"
|
||||
path = "src/lib.rs"
|
||||
# 上游 README 的示例是已过时的 Linux GLX 用法(Session<NeedsConfig> 等类型已不存在),
|
||||
# 直接跑 doctest 会失败。本项目只做 vendor 补丁、不维护文档示例,故关闭。
|
||||
doctest = false
|
||||
|
||||
[[example]]
|
||||
name = "simple_encode"
|
||||
path = "examples/simple_encode.rs"
|
||||
|
||||
[dependencies.bitfields]
|
||||
version = "1.0.2"
|
||||
|
||||
[dependencies.libloading]
|
||||
version = "0.8.9"
|
||||
|
||||
[target."cfg(windows)".dependencies.windows]
|
||||
version = "0.62"
|
||||
features = [
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Graphics_Direct3D",
|
||||
"Win32_Graphics_Direct3D11",
|
||||
]
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "nvenc"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = ["AlsoSylv"]
|
||||
description = "Wrapper for the nvidia NVENC API"
|
||||
rust-version = "1.85.1"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/AlsoSylv/nvenc"
|
||||
license = "MIT"
|
||||
keywords = ["nvidia", "graphics", "media", "video", "ffi"]
|
||||
categories = ["multimedia", "external-ffi-bindings"]
|
||||
|
||||
[dependencies]
|
||||
libloading = "0.8.9"
|
||||
bitfields = "1.0.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies.windows]
|
||||
version = "0.62"
|
||||
features = [
|
||||
"Win32_Graphics_Dxgi_Common",
|
||||
"Win32_Graphics_Direct3D",
|
||||
"Win32_Graphics_Direct3D11",
|
||||
]
|
||||
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
Notice Regarding Standards. I do not provide a license or sublicense to
|
||||
any Intellectual Property Rights relating to any standards, including but not
|
||||
limited to any audio and/or video codec technologies such as MPEG-2, MPEG-4;
|
||||
AVC/H.264; HEVC/H.265; AAC decode/FFMPEG; AAC encode/FFMPEG; VC-1; and MP3
|
||||
(collectively, the "Media Technologies"). For clarity, you will pay any
|
||||
royalties due for such third party technologies, which may include the Media
|
||||
Technologies that are owed as a result of me providing the Software to you.
|
||||
|
||||
Copyright 2025 AlsoSylv
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
### If you like my work and want to support what I do, support me on Ko-Fi 💜!
|
||||
[](https://ko-fi.com/cyberite)
|
||||
---
|
||||
|
||||
  
|
||||
|
||||
The following example is for Linux GLX
|
||||
|
||||
```rust
|
||||
use nvenc::{session::Session, encoder::Encoder};
|
||||
|
||||
fn main() {
|
||||
// Setup GLX context
|
||||
let session: Session<NeedsConfig> = Session::open_gl();
|
||||
let (session, config): (Session<NeedsInit>, NVencPresetConfig)
|
||||
= session.get_preset_config(
|
||||
NV_ENC_CODEC_H264_GUID,
|
||||
NV_ENC_PRESET_P3_GUID,
|
||||
NVencTuningInfo::LowLatency
|
||||
);
|
||||
let init_params = nvenc::session::InitParams {
|
||||
encode_guid: NV_ENC_H264_GUID,
|
||||
preset_guid: NV_ENC_PRESET_P3_GUID,
|
||||
resolution: [1920, 1080],
|
||||
aspect_ratio: [16, 9],
|
||||
frame_rate: [30, 1],
|
||||
tuning_info: NVencTuningInfo::LowLatency,
|
||||
buffer_format: NVencBufferFormat::ARGB,
|
||||
encode_config: &mut config.preset_cfg,
|
||||
enable_ptd: true,
|
||||
};
|
||||
let encoder: Encoder = session.init(init_params);
|
||||
}
|
||||
```
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::time::Instant;
|
||||
|
||||
use nvenc::bitstream::BitStream;
|
||||
use nvenc::session::InitParams;
|
||||
use nvenc::{
|
||||
session::Session,
|
||||
sys::guids::{NV_ENC_CODEC_H264_GUID, NV_ENC_PRESET_P3_GUID},
|
||||
};
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::Graphics::Direct3D11::{ID3D11Device, ID3D11Texture2D};
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn setup() -> ID3D11Device {
|
||||
let dxgi_factory: windows::Win32::Graphics::Dxgi::IDXGIFactory =
|
||||
unsafe { windows::Win32::Graphics::Dxgi::CreateDXGIFactory() }.unwrap();
|
||||
let dxgi_adapter = unsafe { dxgi_factory.EnumAdapters(0) }.unwrap();
|
||||
|
||||
let desc = unsafe { dxgi_adapter.GetDesc() }.unwrap();
|
||||
println!("{:?}", windows::core::HSTRING::from_wide(&desc.Description));
|
||||
|
||||
let mut device = None;
|
||||
let mut device_context = None;
|
||||
unsafe {
|
||||
use windows::Win32::{
|
||||
Foundation::HMODULE,
|
||||
Graphics::{
|
||||
Direct3D::{D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL_11_0},
|
||||
Direct3D11::{D3D11_CREATE_DEVICE_FLAG, D3D11_SDK_VERSION, D3D11CreateDevice},
|
||||
},
|
||||
};
|
||||
|
||||
D3D11CreateDevice(
|
||||
&dxgi_adapter,
|
||||
D3D_DRIVER_TYPE_UNKNOWN,
|
||||
HMODULE(std::ptr::null_mut()),
|
||||
D3D11_CREATE_DEVICE_FLAG(0),
|
||||
Some(&[D3D_FEATURE_LEVEL_11_0]),
|
||||
D3D11_SDK_VERSION,
|
||||
Some(&raw mut device),
|
||||
None,
|
||||
Some(&raw mut device_context),
|
||||
)
|
||||
}
|
||||
.unwrap();
|
||||
let device = device.unwrap();
|
||||
let _device_context = device_context.unwrap();
|
||||
|
||||
device
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn image(device: &ID3D11Device) -> ID3D11Texture2D {
|
||||
let data: [u8; 1920 * 4] = std::array::from_fn(|idx| match idx % 4 {
|
||||
0 => 255,
|
||||
1 => 0,
|
||||
2 => 0,
|
||||
3 => 255,
|
||||
_ => unreachable!(),
|
||||
});
|
||||
|
||||
let mut texture = None;
|
||||
let desc = windows::Win32::Graphics::Direct3D11::D3D11_TEXTURE2D_DESC {
|
||||
Width: 1920,
|
||||
Height: 1080,
|
||||
MipLevels: 1,
|
||||
ArraySize: 1,
|
||||
Format: windows::Win32::Graphics::Dxgi::Common::DXGI_FORMAT_R8G8B8A8_UNORM_SRGB,
|
||||
SampleDesc: windows::Win32::Graphics::Dxgi::Common::DXGI_SAMPLE_DESC {
|
||||
Count: 1,
|
||||
Quality: 0,
|
||||
},
|
||||
Usage: windows::Win32::Graphics::Direct3D11::D3D11_USAGE_DEFAULT,
|
||||
BindFlags: windows::Win32::Graphics::Direct3D11::D3D11_BIND_SHADER_RESOURCE.0 as u32,
|
||||
CPUAccessFlags: windows::Win32::Graphics::Dxgi::Common::DXGI_CPU_ACCESS_NONE,
|
||||
MiscFlags: 0,
|
||||
};
|
||||
let data = windows::Win32::Graphics::Direct3D11::D3D11_SUBRESOURCE_DATA {
|
||||
pSysMem: data.as_ptr() as _,
|
||||
SysMemPitch: 1920 * 4,
|
||||
SysMemSlicePitch: 0,
|
||||
};
|
||||
unsafe {
|
||||
device.CreateTexture2D(
|
||||
&raw const desc,
|
||||
Some(&raw const data),
|
||||
Some(&raw mut texture),
|
||||
)
|
||||
}
|
||||
.unwrap();
|
||||
texture.unwrap()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let device = setup();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
compile_error!("Linux is currently unsupported in this example");
|
||||
#[cfg(windows)]
|
||||
let session: Session<nvenc::session::NeedsConfig> = Session::open_dx(&device).unwrap();
|
||||
assert!(
|
||||
session
|
||||
.get_encode_codecs()
|
||||
.unwrap()
|
||||
.contains(&NV_ENC_CODEC_H264_GUID)
|
||||
);
|
||||
assert!(
|
||||
session
|
||||
.get_encode_presets(NV_ENC_CODEC_H264_GUID)
|
||||
.unwrap()
|
||||
.contains(&NV_ENC_PRESET_P3_GUID)
|
||||
);
|
||||
let (session, mut config) = session
|
||||
.get_encode_preset_config_ex(
|
||||
NV_ENC_CODEC_H264_GUID,
|
||||
NV_ENC_PRESET_P3_GUID,
|
||||
nvenc::sys::enums::NVencTuningInfo::LowLatency,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let texture = image(&device);
|
||||
|
||||
config.preset_cfg.rc_params.rate_control_mode = nvenc::sys::enums::NVencParamsRcMode::VBR;
|
||||
config.preset_cfg.rc_params.average_bit_rate = 10_000_000;
|
||||
config.preset_cfg.gop_len = 0xffffffff;
|
||||
config.preset_cfg.frame_interval_p = 1;
|
||||
println!("P-Frames: {}", config.preset_cfg.frame_interval_p);
|
||||
println!("Bitrate: {}", config.preset_cfg.rc_params.average_bit_rate);
|
||||
|
||||
let init_params = InitParams {
|
||||
encode_guid: NV_ENC_CODEC_H264_GUID,
|
||||
preset_guid: NV_ENC_PRESET_P3_GUID,
|
||||
aspect_ratio: [16, 9],
|
||||
encode_config: &mut config.preset_cfg,
|
||||
tuning_info: nvenc::sys::enums::NVencTuningInfo::LowLatency,
|
||||
buffer_format: nvenc::sys::enums::NVencBufferFormat::ARGB,
|
||||
frame_rate: [30, 1],
|
||||
resolution: [1920, 1080],
|
||||
enable_ptd: true,
|
||||
max_encoder_resolution: [0, 0],
|
||||
};
|
||||
|
||||
println!("FPS: 30");
|
||||
println!("Len: 600 frames");
|
||||
println!("Expected time: {}", 600 / 30);
|
||||
println!("Codec: H264");
|
||||
println!("Tuning Info {:?}:", init_params.tuning_info);
|
||||
|
||||
let encoder = session.init_encoder(init_params).unwrap();
|
||||
let registered = encoder
|
||||
.register_resource_dx11(&texture, nvenc::sys::enums::NVencBufferFormat::ARGB, 0)
|
||||
.unwrap();
|
||||
|
||||
let (processed, to_use) = std::sync::mpsc::sync_channel::<BitStream>(2);
|
||||
let (re_use, to_process) = std::sync::mpsc::sync_channel::<BitStream>(2);
|
||||
processed
|
||||
.send(encoder.create_bitstream_buffer().unwrap())
|
||||
.unwrap();
|
||||
processed
|
||||
.send(encoder.create_bitstream_buffer().unwrap())
|
||||
.unwrap();
|
||||
|
||||
let handle = std::thread::spawn({
|
||||
move || {
|
||||
let mut out = File::create("output.h264").unwrap();
|
||||
|
||||
while let Ok(output) = to_process.recv() {
|
||||
let lock = output.try_lock(true).unwrap();
|
||||
out.write_all(lock.as_slice()).unwrap();
|
||||
drop(lock);
|
||||
if let Err(_) = processed.send(output) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let instant = Instant::now();
|
||||
let mut i = 0;
|
||||
while i < 600 {
|
||||
if let Ok(output) = to_use.recv() {
|
||||
encoder
|
||||
.encode_picture(
|
||||
®istered,
|
||||
&output,
|
||||
i,
|
||||
instant.duration_since(Instant::now()).as_millis() as u64,
|
||||
nvenc::sys::enums::NVencBufferFormat::ARGB,
|
||||
nvenc::sys::enums::NVencPicStruct::Frame,
|
||||
nvenc::sys::enums::NVencPicType::P,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
re_use.send(output).unwrap();
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
drop(to_use);
|
||||
drop(re_use);
|
||||
handle.join().unwrap();
|
||||
}
|
||||
Vendored
+86
@@ -0,0 +1,86 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
use std::{mem::MaybeUninit, sync::OnceLock};
|
||||
|
||||
use libloading::Library;
|
||||
|
||||
use crate::sys::{
|
||||
function_table::NVencFunctionList,
|
||||
result::{NVencError, NVencResult},
|
||||
version::NVENC_API_LIST_VERSION,
|
||||
};
|
||||
|
||||
mod safe;
|
||||
pub use safe::*;
|
||||
/// Contains the original C Types, functions, enums, and constants
|
||||
pub mod sys;
|
||||
|
||||
#[cfg(all(target_arch = "x86_64", target_os = "windows"))]
|
||||
pub const NVENC_DLL: &str = "nvEncodeAPI64.dll";
|
||||
#[cfg(all(target_arch = "x86", target_os = "windows"))]
|
||||
pub const NVENC_DLL: &str = "nvEncodeAPI.dll";
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
/// Platform specific DLL name, `libnvidia-encode.so.1` on Linux, `nvEncodeAPI64.dll` on Windows
|
||||
pub const NVENC_DLL: &str = "libnvidia-encode.so.1";
|
||||
|
||||
#[cfg(all(target_arch = "x86", windows))]
|
||||
#[macro_export]
|
||||
macro_rules! stdcall {
|
||||
(fn $args:tt $(-> $ret:tt)?) => { unsafe extern "stdcall" fn $args $(-> $ret)? };
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(not(all(target_arch = "x86", windows)))]
|
||||
#[macro_export]
|
||||
macro_rules! stdcall {
|
||||
(fn $args:tt $(-> $ret:ty)?) => { unsafe extern "C" fn $args $(-> $ret)? };
|
||||
}
|
||||
|
||||
pub(crate) static LIBRARY: OnceLock<NVENCLibrary> = OnceLock::new();
|
||||
|
||||
/// Struct containing the library handle, init and get_max_version number functions
|
||||
pub struct NVENCLibrary {
|
||||
#[allow(unused)]
|
||||
// Lib must stay alive for nvenc to be used
|
||||
lib: Library,
|
||||
init: stdcall!(fn(list: *mut MaybeUninit<NVencFunctionList>) -> NVencResult),
|
||||
get_max_version: stdcall!(fn(version: *mut u32) -> NVencResult),
|
||||
}
|
||||
|
||||
/// Attempt to init NVENC, returns an error if the library or loading functions cannot be found
|
||||
pub fn nvenc_init() -> Result<&'static NVENCLibrary, libloading::Error> {
|
||||
if let Some(lib) = LIBRARY.get() {
|
||||
Ok(lib)
|
||||
} else {
|
||||
let lib = unsafe { Library::new(NVENC_DLL) }?;
|
||||
let init: stdcall!(fn(function_list: *mut MaybeUninit<NVencFunctionList>) -> NVencResult) =
|
||||
*unsafe { lib.get(b"NvEncodeAPICreateInstance") }?;
|
||||
let get_max_version: stdcall!(fn(version: *mut u32) -> NVencResult) =
|
||||
*unsafe { lib.get(b"NvEncodeAPIGetMaxSupportedVersion") }?;
|
||||
|
||||
Ok(LIBRARY.get_or_init(|| NVENCLibrary {
|
||||
lib,
|
||||
init,
|
||||
get_max_version,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl NVENCLibrary {
|
||||
/// Create a new instance of the NVencFunctionList,
|
||||
pub fn create_instance(&self) -> Result<NVencFunctionList, NVencError> {
|
||||
let mut list = MaybeUninit::zeroed();
|
||||
unsafe { *(list.as_mut_ptr() as *mut u32) = NVENC_API_LIST_VERSION };
|
||||
unsafe { (self.init)(&raw mut list) }.into_error().unwrap();
|
||||
Ok(unsafe { list.assume_init() })
|
||||
}
|
||||
|
||||
pub fn get_max_version(&self) -> Result<u32, NVencError> {
|
||||
let mut version = 0;
|
||||
unsafe { (self.get_max_version)(&raw mut version) }.into_error()?;
|
||||
let minor = version & 0x0F;
|
||||
let version = (version >> 4) | minor << 28;
|
||||
Ok(version)
|
||||
}
|
||||
}
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
use std::{ffi::c_void, sync::Arc};
|
||||
|
||||
use crate::{safe::encoder::EncoderInternal, sys::result::NVencError};
|
||||
|
||||
pub struct BitStream {
|
||||
pub(crate) buffer: *mut c_void,
|
||||
pub(crate) encoder: Arc<EncoderInternal>,
|
||||
}
|
||||
|
||||
impl Drop for BitStream {
|
||||
fn drop(&mut self) {
|
||||
println!("Dropping bitstream buffer");
|
||||
self.encoder.destroy_bitstream_buffer(self.buffer).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl Send for BitStream {}
|
||||
|
||||
impl BitStream {
|
||||
/// Attempts to lock the bit stream, if `wait` is true it will wait
|
||||
/// otherwise a `LockBusy` Error may be returned, in which case the
|
||||
/// client should retry in a few milliseconds
|
||||
pub fn try_lock(&self, wait: bool) -> Result<BitStreamLockGuard<'_>, NVencError> {
|
||||
let lock = self.encoder.lock_bit_stream_buffer(self.buffer, wait)?;
|
||||
Ok(BitStreamLockGuard {
|
||||
buffer: self,
|
||||
data_ptr: lock.bitstream_buffer,
|
||||
data_len: lock.bitstream_size_in_bytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds a reference to the `BitStream` and holds the data and associated fields
|
||||
pub struct BitStreamLockGuard<'a> {
|
||||
buffer: &'a BitStream,
|
||||
data_ptr: *mut c_void,
|
||||
data_len: u32,
|
||||
}
|
||||
|
||||
impl BitStreamLockGuard<'_> {
|
||||
pub fn as_slice(&self) -> &[u8] {
|
||||
unsafe { std::slice::from_raw_parts(self.data_ptr as _, self.data_len as _) }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Drop for BitStreamLockGuard<'a> {
|
||||
fn drop(&mut self) {
|
||||
self.buffer
|
||||
.encoder
|
||||
.unlock_bit_stream_buffer(self.buffer.buffer)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
Vendored
+517
@@ -0,0 +1,517 @@
|
||||
use std::ffi::{CStr, c_void};
|
||||
use std::marker::PhantomData;
|
||||
use std::mem::ManuallyDrop;
|
||||
use std::sync::Arc;
|
||||
use std::{mem::MaybeUninit, ptr::NonNull};
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows::Win32::Graphics::Direct3D11::ID3D11Texture2D;
|
||||
|
||||
use crate::input_buffer::InputBuffer;
|
||||
use crate::safe::bitstream::BitStream;
|
||||
use crate::sys::enums::{
|
||||
NVencBufferFormat, NVencBufferUsage, NVencInputResourceType, NVencMemoryHeap, NVencPicStruct,
|
||||
NVencPicType,
|
||||
};
|
||||
use crate::sys::structs::{
|
||||
Guid, NV_ENC_CREATE_INPUT_BUFFER_VER, NV_ENC_LOCK_INPUT_BUFFER_VER, NVencCodecPicParams,
|
||||
NVencCreateInputBuffer, NVencLockInputBuffer, NVencPicParamsH264, NVencRegisterResource,
|
||||
};
|
||||
use crate::sys::{
|
||||
enums::{NVencPicFlags, NVencTuningInfo},
|
||||
function_table::NVencFunctionList,
|
||||
result::NVencError,
|
||||
structs::{
|
||||
NV_ENC_CONFIG_VER, NV_ENC_CREATE_BITSTREAM_BUFFER_VER, NV_ENC_LOCK_BITSTREAM_VER,
|
||||
NV_ENC_MAP_INPUT_RESOURCE_VER, NV_ENC_PIC_PARAMS_VER, NV_ENC_PRESET_CONFIG_VER,
|
||||
NV_ENC_REGISTER_RESOURCE_VER, NVencCreateBitstreamBuffer, NVencInitializeParams,
|
||||
NVencLockBitStream, NVencPicParams, NVencPresetConfig,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct Encoder {
|
||||
pub(crate) encoder: Arc<EncoderInternal>,
|
||||
pub(crate) marker: PhantomData<*mut ()>,
|
||||
}
|
||||
|
||||
pub trait EncoderInput {
|
||||
#[doc(hidden)]
|
||||
fn as_ptr(&self) -> *mut c_void;
|
||||
|
||||
fn pitch(&self) -> u32;
|
||||
fn height(&self) -> u32;
|
||||
fn width(&self) -> u32;
|
||||
}
|
||||
|
||||
impl EncoderInput for RegisteredResource {
|
||||
fn as_ptr(&self) -> *mut c_void {
|
||||
self.mapped
|
||||
}
|
||||
|
||||
fn pitch(&self) -> u32 {
|
||||
self.pitch
|
||||
}
|
||||
|
||||
fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderInput for InputBuffer {
|
||||
fn as_ptr(&self) -> *mut c_void {
|
||||
self.buffer
|
||||
}
|
||||
|
||||
fn pitch(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn height(&self) -> u32 {
|
||||
self.height
|
||||
}
|
||||
|
||||
fn width(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder {
|
||||
#[cfg(windows)]
|
||||
/// Registers a DX11 texture as an nvenc input resource
|
||||
pub fn register_resource_dx11(
|
||||
&self,
|
||||
resource: &ID3D11Texture2D,
|
||||
format: NVencBufferFormat,
|
||||
pitch: u32,
|
||||
) -> Result<RegisteredResource, NVencError> {
|
||||
self.encoder.register_resource_dx(resource, format, pitch)
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// `resource` must not be null
|
||||
/// `ty` and `format` should match the original buffer
|
||||
pub unsafe fn register_resource_raw(
|
||||
&self,
|
||||
resource: *mut c_void,
|
||||
format: NVencBufferFormat,
|
||||
usage: NVencBufferUsage,
|
||||
ty: NVencInputResourceType,
|
||||
resolution: [u32; 2],
|
||||
pitch: u32,
|
||||
) -> Result<*mut c_void, NVencError> {
|
||||
unsafe {
|
||||
self.encoder
|
||||
.register_resource_raw(resource, format, usage, ty, resolution, pitch)
|
||||
}
|
||||
}
|
||||
|
||||
/// Takes an encoder inout, and outputs a bit stream that can be read back
|
||||
pub fn encode_picture<I: EncoderInput>(
|
||||
&self,
|
||||
input: &I,
|
||||
output: &BitStream,
|
||||
frame_count: usize,
|
||||
timestamp: u64,
|
||||
format: NVencBufferFormat,
|
||||
pic_struct: NVencPicStruct,
|
||||
ty: NVencPicType,
|
||||
codec_params: Option<CodecPicParams>,
|
||||
) -> Result<(), NVencError> {
|
||||
self.encoder.encode_picture(
|
||||
input,
|
||||
output,
|
||||
frame_count,
|
||||
timestamp,
|
||||
format,
|
||||
pic_struct,
|
||||
ty,
|
||||
codec_params,
|
||||
)
|
||||
}
|
||||
|
||||
/// Ends the encoder session
|
||||
pub fn end_encode(&self) -> Result<(), NVencError> {
|
||||
self.encoder.end_encode()
|
||||
}
|
||||
|
||||
/// Creates a bitstream bfufer that can be used for output storage
|
||||
pub fn create_bitstream_buffer(&self) -> Result<BitStream, NVencError> {
|
||||
self.encoder.create_bitstream_buffer()
|
||||
}
|
||||
|
||||
pub fn create_input_buffer(
|
||||
&self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
memory_heap: NVencMemoryHeap,
|
||||
format: NVencBufferFormat,
|
||||
) -> Result<InputBuffer, NVencError> {
|
||||
Ok(InputBuffer {
|
||||
encoder: self.encoder.clone(),
|
||||
buffer: self
|
||||
.encoder
|
||||
.create_input_buffer(width, height, memory_heap, format)?,
|
||||
width,
|
||||
height,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct EncoderInternal {
|
||||
pub(super) encoder: NonNull<c_void>,
|
||||
pub(super) function_list: NVencFunctionList,
|
||||
}
|
||||
|
||||
/// The Nvidia encoder is technically thread-safe
|
||||
unsafe impl Send for EncoderInternal {}
|
||||
|
||||
/// The Nvidia encoder handles syncing via `lock_bit_stream` which is all that is allowed to be Send
|
||||
unsafe impl Sync for EncoderInternal {}
|
||||
|
||||
impl Drop for EncoderInternal {
|
||||
fn drop(&mut self) {
|
||||
println!("Dropping encoder, this should happen last");
|
||||
unsafe { (self.function_list.nvenc_destroy_encoder)(self.encoder.as_ptr()) };
|
||||
}
|
||||
}
|
||||
|
||||
impl EncoderInternal {
|
||||
pub(crate) fn get_encode_preset_config_ex(
|
||||
&self,
|
||||
codec: Guid,
|
||||
preset: Guid,
|
||||
tuning_info: NVencTuningInfo,
|
||||
) -> Result<NVencPresetConfig, NVencError> {
|
||||
let mut config: std::mem::MaybeUninit<NVencPresetConfig> = std::mem::MaybeUninit::zeroed();
|
||||
unsafe { &mut *config.as_mut_ptr() }.version = NV_ENC_PRESET_CONFIG_VER;
|
||||
unsafe { &mut *config.as_mut_ptr() }.preset_cfg.version = NV_ENC_CONFIG_VER;
|
||||
unsafe {
|
||||
(self.function_list.nvenc_get_encode_preset_config_ex)(
|
||||
self.encoder.as_ptr(),
|
||||
codec,
|
||||
preset,
|
||||
tuning_info,
|
||||
&raw mut config,
|
||||
)
|
||||
.into_error()?
|
||||
};
|
||||
|
||||
Ok(unsafe { config.assume_init() })
|
||||
}
|
||||
|
||||
pub(crate) fn init_encoder(
|
||||
&self,
|
||||
mut init_params: NVencInitializeParams,
|
||||
) -> Result<(), NVencError> {
|
||||
unsafe {
|
||||
(self.function_list.nvenc_initialize_encoder)(
|
||||
self.encoder.as_ptr(),
|
||||
&raw mut init_params,
|
||||
)
|
||||
}
|
||||
.into_error()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
pub fn register_resource_dx(
|
||||
self: &Arc<EncoderInternal>,
|
||||
resource: &ID3D11Texture2D,
|
||||
format: NVencBufferFormat,
|
||||
pitch: u32,
|
||||
) -> Result<RegisteredResource, NVencError> {
|
||||
use windows::Win32::Graphics::Direct3D11::D3D11_TEXTURE2D_DESC;
|
||||
use windows::core::Interface;
|
||||
|
||||
let mut desc = D3D11_TEXTURE2D_DESC::default();
|
||||
unsafe { resource.GetDesc(&raw mut desc) };
|
||||
|
||||
let registered = unsafe {
|
||||
self.register_resource_raw(
|
||||
resource.as_raw(),
|
||||
NVencBufferFormat::Undefined,
|
||||
NVencBufferUsage::Image,
|
||||
NVencInputResourceType::DirectX,
|
||||
[desc.Width, desc.Height],
|
||||
pitch,
|
||||
)?
|
||||
};
|
||||
|
||||
let mapped = unsafe { self.map_input_resource_raw(registered, format)? };
|
||||
|
||||
Ok(RegisteredResource {
|
||||
encoder: self.clone(),
|
||||
pitch,
|
||||
width: desc.Width,
|
||||
height: desc.Height,
|
||||
registered,
|
||||
mapped,
|
||||
})
|
||||
}
|
||||
|
||||
pub unsafe fn map_input_resource_raw(
|
||||
&self,
|
||||
resource: *mut c_void,
|
||||
format: NVencBufferFormat,
|
||||
) -> Result<*mut c_void, NVencError> {
|
||||
let mut map: crate::sys::structs::NVencMapInputResource = unsafe { std::mem::zeroed() };
|
||||
map.version = NV_ENC_MAP_INPUT_RESOURCE_VER;
|
||||
map.registered_resource = resource;
|
||||
map.mapped_buffer_format = format;
|
||||
|
||||
unsafe {
|
||||
(self.function_list.nvenc_map_input_resource)(self.encoder.as_ptr(), &raw mut map)
|
||||
}
|
||||
.into_error()?;
|
||||
|
||||
Ok(map.mapped_resource)
|
||||
}
|
||||
|
||||
pub unsafe fn register_resource_raw(
|
||||
&self,
|
||||
resource: *mut c_void,
|
||||
format: NVencBufferFormat,
|
||||
usage: NVencBufferUsage,
|
||||
ty: NVencInputResourceType,
|
||||
resolution: [u32; 2],
|
||||
pitch: u32,
|
||||
) -> Result<*mut c_void, NVencError> {
|
||||
let mut register_resource: NVencRegisterResource = unsafe { std::mem::zeroed() };
|
||||
register_resource.buffer_format = format;
|
||||
register_resource.buffer_usage = usage;
|
||||
register_resource.resource_type = ty;
|
||||
register_resource.version = NV_ENC_REGISTER_RESOURCE_VER;
|
||||
register_resource.width = resolution[0];
|
||||
register_resource.height = resolution[1];
|
||||
register_resource.pitch = pitch;
|
||||
register_resource.sub_resource_index = 0;
|
||||
register_resource.resource_to_register = resource;
|
||||
unsafe {
|
||||
(self.function_list.nvenc_register_resource)(
|
||||
self.encoder.as_ptr(),
|
||||
&raw mut register_resource,
|
||||
)
|
||||
.into_error()
|
||||
}?;
|
||||
Ok(register_resource.registered_resource)
|
||||
}
|
||||
|
||||
pub unsafe fn unmap_input_resource(
|
||||
&self,
|
||||
mapped_resource: *mut c_void,
|
||||
) -> Result<(), NVencError> {
|
||||
unsafe {
|
||||
(self.function_list.nvenc_unmap_input_resource)(self.encoder.as_ptr(), mapped_resource)
|
||||
}
|
||||
.into_error()
|
||||
}
|
||||
|
||||
pub unsafe fn unregister_resource(
|
||||
&self,
|
||||
registered_resource: *mut c_void,
|
||||
) -> Result<(), NVencError> {
|
||||
unsafe {
|
||||
(self.function_list.nvenc_unregister_resource)(
|
||||
self.encoder.as_ptr(),
|
||||
registered_resource,
|
||||
)
|
||||
}
|
||||
.into_error()
|
||||
}
|
||||
|
||||
pub fn create_input_buffer(
|
||||
&self,
|
||||
width: u32,
|
||||
height: u32,
|
||||
memory_heap: NVencMemoryHeap,
|
||||
format: NVencBufferFormat,
|
||||
) -> Result<*mut c_void, NVencError> {
|
||||
let mut input = NVencCreateInputBuffer {
|
||||
version: NV_ENC_CREATE_INPUT_BUFFER_VER,
|
||||
width: width,
|
||||
height: height,
|
||||
memory_heap,
|
||||
buffer_fmt: format,
|
||||
rsvd: 0,
|
||||
input_buffer: std::ptr::null_mut(),
|
||||
p_sys_mem_buffer: std::ptr::null_mut(),
|
||||
rsvd1: unsafe { std::mem::zeroed() },
|
||||
rsvd2: unsafe { std::mem::zeroed() },
|
||||
};
|
||||
unsafe {
|
||||
(self.function_list.nvenc_create_input_buffer)(self.encoder.as_ptr(), &raw mut input)
|
||||
.into_error()?
|
||||
};
|
||||
|
||||
Ok(input.input_buffer)
|
||||
}
|
||||
|
||||
pub fn destroy_input_buffer(&self, buffer: *mut c_void) -> Result<(), NVencError> {
|
||||
unsafe { (self.function_list.nvenc_destroy_input_buffer)(self.encoder.as_ptr(), buffer) }
|
||||
.into_error()
|
||||
}
|
||||
|
||||
pub fn lock_input_buffer(&self, buffer: *mut c_void) -> Result<(*mut c_void, u32), NVencError> {
|
||||
let mut lock = NVencLockInputBuffer {
|
||||
version: NV_ENC_LOCK_INPUT_BUFFER_VER,
|
||||
bitflags: 0,
|
||||
input_buffer: buffer,
|
||||
buffer_data_ptr: std::ptr::null_mut(),
|
||||
pitch: 0,
|
||||
rsvd1: unsafe { std::mem::zeroed() },
|
||||
rsvd2: unsafe { std::mem::zeroed() },
|
||||
};
|
||||
|
||||
unsafe {
|
||||
(self.function_list.nvenc_lock_input_buffer)(self.encoder.as_ptr(), &raw mut lock)
|
||||
.into_error()?;
|
||||
}
|
||||
|
||||
Ok((lock.buffer_data_ptr, lock.pitch))
|
||||
}
|
||||
|
||||
pub fn unlock_input_buffer(&self, locked_buffer: *mut c_void) -> Result<(), NVencError> {
|
||||
unsafe {
|
||||
(self.function_list.nvenc_unlock_input_buffer)(self.encoder.as_ptr(), locked_buffer)
|
||||
.into_error()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode_picture<I: EncoderInput>(
|
||||
&self,
|
||||
input: &I,
|
||||
output: &BitStream,
|
||||
frame_count: usize,
|
||||
timestamp: u64,
|
||||
format: NVencBufferFormat,
|
||||
pic_struct: NVencPicStruct,
|
||||
ty: NVencPicType,
|
||||
codec_params: Option<CodecPicParams>,
|
||||
) -> Result<(), NVencError> {
|
||||
let mut params: MaybeUninit<NVencPicParams> = MaybeUninit::zeroed();
|
||||
let sys_codec = match codec_params {
|
||||
None => NVencCodecPicParams { rsvd: [0; 256] },
|
||||
Some(CodecPicParams::H264(codec)) => NVencCodecPicParams {
|
||||
h264_pic_params: ManuallyDrop::new(codec),
|
||||
},
|
||||
};
|
||||
unsafe { &mut *params.as_mut_ptr() }.version = NV_ENC_PIC_PARAMS_VER;
|
||||
unsafe { &mut *params.as_mut_ptr() }.input_width = input.width();
|
||||
unsafe { &mut *params.as_mut_ptr() }.input_height = input.height();
|
||||
unsafe { &mut *params.as_mut_ptr() }.input_pitch = input.pitch();
|
||||
unsafe { &mut *params.as_mut_ptr() }.output_bitstream = output.buffer;
|
||||
unsafe { &mut *params.as_mut_ptr() }.input_buffer = input.as_ptr();
|
||||
unsafe { &mut *params.as_mut_ptr() }.buffer_format = format;
|
||||
unsafe { &mut *params.as_mut_ptr() }.picture_struct = pic_struct;
|
||||
// [本项目补丁] 只设 picture_type=IDR 不足以让 NVENC 真正输出 IDR 切片,
|
||||
// 必须同时置 encode_pic_flags |= NV_ENC_PIC_FLAG_FORCEIDR。
|
||||
//
|
||||
// 上游 0.1.0 未设置该标志,导致运行期请求关键帧时编码器静默忽略、
|
||||
// 继续输出 P 帧(实测:picture_type=IDR 但输出 NAL type=1)。
|
||||
// 首帧之所以"看起来正常",是因为 frame_idx==0 本来就强制 IDR,与本字段无关。
|
||||
//
|
||||
// 语义区分:IntraRefresh 走 ForceIntra,其余 IDR 类走 ForceIDR。
|
||||
// 必须在 `picture_type = ty` 之前计算,否则 ty 已被移动。
|
||||
let pic_flags = match &ty {
|
||||
NVencPicType::IDR => NVencPicFlags::ForceIDR as u32,
|
||||
NVencPicType::IntraRefresh => NVencPicFlags::ForceIntra as u32,
|
||||
_ => 0,
|
||||
};
|
||||
unsafe { &mut *params.as_mut_ptr() }.picture_type = ty;
|
||||
unsafe { &mut *params.as_mut_ptr() }.encode_pic_flags = pic_flags;
|
||||
unsafe { &mut *params.as_mut_ptr() }.frame_idx = frame_count as u32;
|
||||
unsafe { &mut *params.as_mut_ptr() }.input_time_stamp = timestamp;
|
||||
unsafe { &mut *params.as_mut_ptr() }.codec_pic_params = sys_codec;
|
||||
match unsafe {
|
||||
(self.function_list.nvenc_encode_picture)(self.encoder.as_ptr(), &raw mut params)
|
||||
.into_error()
|
||||
} {
|
||||
Err(err) => {
|
||||
let ptr =
|
||||
unsafe { (self.function_list.nvenc_get_last_error)(self.encoder.as_ptr()) };
|
||||
println!("{:?}", unsafe { CStr::from_ptr(ptr) });
|
||||
Err(err)
|
||||
}
|
||||
Ok(()) => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_encode(&self) -> Result<(), NVencError> {
|
||||
let mut params: MaybeUninit<NVencPicParams> = MaybeUninit::zeroed();
|
||||
unsafe { &mut *params.as_mut_ptr() }.version = NV_ENC_PIC_PARAMS_VER;
|
||||
unsafe { &mut *params.as_mut_ptr() }.encode_pic_flags = NVencPicFlags::Eos as u32;
|
||||
unsafe {
|
||||
(self.function_list.nvenc_encode_picture)(self.encoder.as_ptr(), &raw mut params)
|
||||
.into_error()
|
||||
}?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn lock_bit_stream_buffer(
|
||||
&self,
|
||||
buffer: *mut c_void,
|
||||
wait: bool,
|
||||
) -> Result<NVencLockBitStream, NVencError> {
|
||||
let mut lock: MaybeUninit<NVencLockBitStream> = MaybeUninit::zeroed();
|
||||
unsafe { &mut *lock.as_mut_ptr() }.version = NV_ENC_LOCK_BITSTREAM_VER;
|
||||
unsafe { &mut *lock.as_mut_ptr() }.output_bit_stream = buffer;
|
||||
unsafe { &mut *lock.as_mut_ptr() }
|
||||
.bit_fields
|
||||
.set_do_not_wait(!wait);
|
||||
unsafe { (self.function_list.nvenc_lock_bit_stream)(self.encoder.as_ptr(), &raw mut lock) }
|
||||
.into_error()?;
|
||||
Ok(unsafe { lock.assume_init() })
|
||||
}
|
||||
|
||||
pub(crate) fn unlock_bit_stream_buffer(&self, buffer: *mut c_void) -> Result<(), NVencError> {
|
||||
unsafe { (self.function_list.nvenc_unlock_bit_stream)(self.encoder.as_ptr(), buffer) }
|
||||
.into_error()
|
||||
}
|
||||
|
||||
pub fn create_bitstream_buffer(self: &Arc<Self>) -> Result<BitStream, NVencError> {
|
||||
let mut params: NVencCreateBitstreamBuffer = unsafe { std::mem::zeroed() };
|
||||
params.version = NV_ENC_CREATE_BITSTREAM_BUFFER_VER;
|
||||
unsafe {
|
||||
(self.function_list.nvenc_create_bit_stream_buffer)(
|
||||
self.encoder.as_ptr(),
|
||||
&raw mut params,
|
||||
)
|
||||
}
|
||||
.into_error()?;
|
||||
Ok(BitStream {
|
||||
buffer: params.bitstream_buffer,
|
||||
encoder: self.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn destroy_bitstream_buffer(&self, buffer: *mut c_void) -> Result<(), NVencError> {
|
||||
unsafe {
|
||||
(self.function_list.nvenc_destory_bit_stream_buffer)(self.encoder.as_ptr(), buffer)
|
||||
}
|
||||
.into_error()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum CodecPicParams {
|
||||
H264(NVencPicParamsH264),
|
||||
}
|
||||
|
||||
pub struct RegisteredResource {
|
||||
registered: *mut c_void,
|
||||
mapped: *mut c_void,
|
||||
pitch: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
encoder: Arc<EncoderInternal>,
|
||||
}
|
||||
|
||||
impl Drop for RegisteredResource {
|
||||
fn drop(&mut self) {
|
||||
unsafe { self.encoder.unmap_input_resource(self.mapped).unwrap() };
|
||||
unsafe { self.encoder.unregister_resource(self.registered).unwrap() };
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
use std::{ffi::c_void, sync::Arc};
|
||||
|
||||
use crate::{encoder::EncoderInternal, sys::result::NVencError};
|
||||
|
||||
pub struct InputBuffer {
|
||||
pub(crate) encoder: Arc<EncoderInternal>,
|
||||
pub(crate) buffer: *mut c_void,
|
||||
pub(crate) width: u32,
|
||||
pub(crate) height: u32,
|
||||
}
|
||||
|
||||
impl InputBuffer {
|
||||
pub fn lock(&self) -> Result<InputBufferLock<'_>, NVencError> {
|
||||
let (lock_ptr, pitch) = self.encoder.lock_input_buffer(self.buffer)?;
|
||||
Ok(InputBufferLock {
|
||||
input_buffer: self,
|
||||
lock_ptr,
|
||||
pitch,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for InputBuffer {
|
||||
fn drop(&mut self) {
|
||||
self.encoder.destroy_input_buffer(self.buffer).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InputBufferLock<'a> {
|
||||
input_buffer: &'a InputBuffer,
|
||||
lock_ptr: *mut c_void,
|
||||
pitch: u32,
|
||||
}
|
||||
|
||||
impl InputBufferLock<'_> {
|
||||
pub unsafe fn data_ptr(&self) -> *mut u8 {
|
||||
self.lock_ptr as _
|
||||
}
|
||||
|
||||
pub fn width(&self) -> u32 {
|
||||
self.input_buffer.width
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u32 {
|
||||
self.input_buffer.height
|
||||
}
|
||||
|
||||
pub fn pitch(&self) -> u32 {
|
||||
self.pitch
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for InputBufferLock<'_> {
|
||||
fn drop(&mut self) {
|
||||
self.input_buffer
|
||||
.encoder
|
||||
.unlock_input_buffer(self.lock_ptr)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
/// Contains the type for the safe BitStream buffer wrapper
|
||||
pub mod bitstream;
|
||||
/// Wrapper for the NVEncoder after creating an active session
|
||||
pub mod encoder;
|
||||
/// Wrapper around an NVenc input buffer
|
||||
pub mod input_buffer;
|
||||
/// Type-state wrapper around session initialization
|
||||
pub mod session;
|
||||
Vendored
+198
@@ -0,0 +1,198 @@
|
||||
use std::{marker::PhantomData, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
encoder::{Encoder, EncoderInternal},
|
||||
sys::{
|
||||
enums::{NVencBufferFormat, NVencDeviceType, NVencTuningInfo},
|
||||
result::NVencError,
|
||||
structs::{
|
||||
Guid, NV_ENC_INITIALIZE_PARAMS_VER, NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER,
|
||||
NVencConfig, NVencInitializeParams, NVencOpenEncodeSessionExParams, NVencPresetConfig,
|
||||
},
|
||||
version::NVENC_API_VERSION,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct Session<T> {
|
||||
encoder: Arc<EncoderInternal>,
|
||||
p: PhantomData<T>,
|
||||
}
|
||||
|
||||
pub struct NeedsConfig;
|
||||
|
||||
pub struct NeedsInit;
|
||||
|
||||
#[cfg(windows)]
|
||||
use windows::core::Interface;
|
||||
|
||||
impl Session<NeedsConfig> {
|
||||
#[cfg(windows)]
|
||||
pub fn open_dx(device: &impl Interface) -> Result<Self, NVencError> {
|
||||
let lib = crate::nvenc_init().unwrap();
|
||||
let function_list = lib.create_instance()?;
|
||||
let mut session_params: NVencOpenEncodeSessionExParams = unsafe { std::mem::zeroed() };
|
||||
session_params.version = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER;
|
||||
session_params.device_type = NVencDeviceType::DirectX;
|
||||
session_params.api_version = NVENC_API_VERSION;
|
||||
session_params.device = device.as_raw();
|
||||
let mut encoder = std::ptr::null_mut();
|
||||
unsafe {
|
||||
(function_list.nvenc_open_encode_session_ex)(&raw mut session_params, &raw mut encoder)
|
||||
.into_error()
|
||||
}?;
|
||||
assert!(!encoder.is_null());
|
||||
let encoder = EncoderInternal {
|
||||
encoder: unsafe { std::ptr::NonNull::new_unchecked(encoder) },
|
||||
function_list,
|
||||
};
|
||||
Ok(Self {
|
||||
encoder: Arc::new(encoder),
|
||||
p: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_gl() -> Result<Self, NVencError> {
|
||||
let lib = crate::nvenc_init().unwrap();
|
||||
let function_list = lib.create_instance()?;
|
||||
let mut session_params: NVencOpenEncodeSessionExParams = unsafe { std::mem::zeroed() };
|
||||
session_params.version = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER;
|
||||
session_params.device_type = NVencDeviceType::OpenGL;
|
||||
session_params.api_version = NVENC_API_VERSION;
|
||||
session_params.device = std::ptr::null_mut();
|
||||
|
||||
let mut encoder = std::ptr::null_mut();
|
||||
unsafe {
|
||||
(function_list.nvenc_open_encode_session_ex)(&raw mut session_params, &raw mut encoder)
|
||||
.into_error()
|
||||
}?;
|
||||
assert!(!encoder.is_null());
|
||||
let encoder = EncoderInternal {
|
||||
encoder: unsafe { std::ptr::NonNull::new_unchecked(encoder) },
|
||||
function_list,
|
||||
};
|
||||
Ok(Self {
|
||||
encoder: Arc::new(encoder),
|
||||
p: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_encode_codecs(&self) -> Result<Box<[Guid]>, NVencError> {
|
||||
let mut count = 0;
|
||||
unsafe {
|
||||
(self.encoder.function_list.nvenc_get_encoder_guid_count)(
|
||||
self.encoder.encoder.as_ptr(),
|
||||
&raw mut count,
|
||||
)
|
||||
.into_error()
|
||||
}?;
|
||||
let mut array = vec![Guid::default(); count as usize];
|
||||
unsafe {
|
||||
(self.encoder.function_list.nvenc_get_encoder_guids)(
|
||||
self.encoder.encoder.as_ptr(),
|
||||
array.as_mut_ptr(),
|
||||
count,
|
||||
&raw mut count,
|
||||
)
|
||||
}
|
||||
.into_error()?;
|
||||
array.truncate(count as usize);
|
||||
Ok(array.into())
|
||||
}
|
||||
|
||||
pub fn get_encode_presets(&self, codec: Guid) -> Result<Box<[Guid]>, NVencError> {
|
||||
let mut count = 0;
|
||||
unsafe {
|
||||
(self.encoder.function_list.nvenc_get_encode_preset_count)(
|
||||
self.encoder.encoder.as_ptr(),
|
||||
codec.clone(),
|
||||
&raw mut count,
|
||||
)
|
||||
.into_error()
|
||||
}?;
|
||||
let mut array = vec![Guid::default(); count as usize];
|
||||
unsafe {
|
||||
(self.encoder.function_list.nvenc_get_encode_preset_guids)(
|
||||
self.encoder.encoder.as_ptr(),
|
||||
codec,
|
||||
array.as_mut_ptr(),
|
||||
count,
|
||||
&raw mut count,
|
||||
)
|
||||
}
|
||||
.into_error()?;
|
||||
array.truncate(count as usize);
|
||||
Ok(array.into())
|
||||
}
|
||||
|
||||
pub fn get_encode_preset_config_ex(
|
||||
self,
|
||||
codec: Guid,
|
||||
preset: Guid,
|
||||
tuning_info: NVencTuningInfo,
|
||||
) -> Result<(Session<NeedsInit>, NVencPresetConfig), NVencError> {
|
||||
self.encoder
|
||||
.get_encode_preset_config_ex(codec, preset, tuning_info)
|
||||
.map(|config| {
|
||||
(
|
||||
Session {
|
||||
encoder: self.encoder,
|
||||
p: PhantomData,
|
||||
},
|
||||
config,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InitParams<'a> {
|
||||
pub encode_guid: Guid,
|
||||
pub preset_guid: Guid,
|
||||
pub resolution: [u32; 2],
|
||||
pub aspect_ratio: [u32; 2],
|
||||
pub frame_rate: [u32; 2],
|
||||
pub tuning_info: NVencTuningInfo,
|
||||
pub buffer_format: NVencBufferFormat,
|
||||
pub encode_config: &'a mut NVencConfig,
|
||||
pub enable_ptd: bool,
|
||||
pub max_encoder_resolution: [u32; 2],
|
||||
// TODO: Support for async encoding and bit fields
|
||||
}
|
||||
|
||||
impl Session<NeedsInit> {
|
||||
pub fn init_encoder(self, init_params: InitParams) -> Result<Encoder, NVencError> {
|
||||
// This sucks, lol
|
||||
let raw_init_params = NVencInitializeParams {
|
||||
encode_guid: init_params.encode_guid,
|
||||
preset_guid: init_params.preset_guid,
|
||||
encode_width: init_params.resolution[0],
|
||||
encode_height: init_params.resolution[1],
|
||||
dar_width: init_params.aspect_ratio[0],
|
||||
dar_height: init_params.aspect_ratio[1],
|
||||
frame_rate_num: init_params.frame_rate[0],
|
||||
frame_rate_den: init_params.frame_rate[1],
|
||||
tuning_info: init_params.tuning_info,
|
||||
buffer_format: init_params.buffer_format,
|
||||
encode_config: init_params.encode_config as _,
|
||||
enable_ptd: init_params.enable_ptd as u32,
|
||||
version: NV_ENC_INITIALIZE_PARAMS_VER,
|
||||
enable_encode_async: 0,
|
||||
bit_flags: crate::sys::structs::NVencInitializeParamsBitfields::new(),
|
||||
priv_data_size: 0,
|
||||
rsvd: 0,
|
||||
priv_data: std::ptr::null_mut(),
|
||||
max_encode_width: init_params.max_encoder_resolution[0],
|
||||
max_encode_height: init_params.max_encoder_resolution[1],
|
||||
max_me_hint_counts_per_block: [Default::default(), Default::default()],
|
||||
num_state_buffers: 0,
|
||||
output_stats_level: crate::sys::enums::NVencOutputStatsLevel::None,
|
||||
rsvd1: unsafe { std::mem::zeroed() },
|
||||
rsvd2: unsafe { std::mem::zeroed() },
|
||||
};
|
||||
|
||||
self.encoder.init_encoder(raw_init_params)?;
|
||||
Ok(Encoder {
|
||||
encoder: self.encoder,
|
||||
marker: PhantomData,
|
||||
})
|
||||
}
|
||||
}
|
||||
Vendored
+481
@@ -0,0 +1,481 @@
|
||||
#[repr(C)]
|
||||
pub enum NVencParamsFrameFieldMode {
|
||||
Frame = 0x01,
|
||||
Field = 0x02,
|
||||
MBAFF = 0x03,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencParamsRcMode {
|
||||
ConstQP = 0x00,
|
||||
VBR = 0x01,
|
||||
CBR = 0x02,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencMultiPass {
|
||||
Disabled = 0x00,
|
||||
TwoPassQuarterResolution = 0x01,
|
||||
TwoPassFullResolution = 0x02,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencStateRestoreType {
|
||||
Full = 0x01,
|
||||
RateControl = 0x02,
|
||||
Encode = 0x03,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencOutputStatsLevel {
|
||||
None = 0,
|
||||
BlockLevel = 1,
|
||||
RowLevel = 2,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencEmphasisMapLevel {
|
||||
Level0 = 0x0,
|
||||
Level1 = 0x1,
|
||||
Level2 = 0x2,
|
||||
Level3 = 0x3,
|
||||
Level4 = 0x4,
|
||||
Level5 = 0x5,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencQPMapMode {
|
||||
Disabled = 0x0,
|
||||
Emphasis = 0x1,
|
||||
Delta = 0x2,
|
||||
/// Currently unsupported as of NVENC 13.0
|
||||
Map = 0x3,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencPicStruct {
|
||||
Frame = 0x01,
|
||||
FieldTopBottom = 0x02,
|
||||
FieldBottomTop = 0x03,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencDisplayPicStruct {
|
||||
Frame = 0x0,
|
||||
FieldTopBottom = 0x1,
|
||||
FieldBottomTop = 0x2,
|
||||
Doubling = 0x3,
|
||||
Tripling = 0x4,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencPicType {
|
||||
P = 0x00,
|
||||
B = 0x01,
|
||||
IDR = 0x03,
|
||||
BI = 0x04,
|
||||
Skipped = 0x05,
|
||||
IntraRefresh = 0x06,
|
||||
NonRefP = 0x07,
|
||||
Switch = 0x08,
|
||||
UNKNOWN = 0xFF,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencMVPrecision {
|
||||
Default = 0x00,
|
||||
FullPel = 0x01,
|
||||
HalfPel = 0x02,
|
||||
QuarterPel = 0x03,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencBufferFormat {
|
||||
Undefined = 0x00000000,
|
||||
NV12 = 0x00000001,
|
||||
YV12 = 0x00000010,
|
||||
IYUV = 0x00000100,
|
||||
YUV444 = 0x00001000,
|
||||
YUV420_10Bit = 0x00010000,
|
||||
YUV444_10Bit = 0x00100000,
|
||||
ARGB = 0x01000000,
|
||||
ARGB10 = 0x02000000,
|
||||
AYUV = 0x04000000,
|
||||
ABGR = 0x10000000,
|
||||
ABGR10 = 0x20000000,
|
||||
U8 = 0x40000000,
|
||||
NV16 = 0x40000001,
|
||||
P210 = 0x40000002,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NvencLevel {
|
||||
AutoSelect = 0,
|
||||
}
|
||||
|
||||
// TODO: Translate level macro
|
||||
#[repr(C)]
|
||||
pub enum H264Level {
|
||||
H264_1 = 10,
|
||||
H264_1b = 9,
|
||||
H264_11 = 11,
|
||||
H264_12 = 12,
|
||||
H264_13 = 13,
|
||||
H264_2 = 20,
|
||||
H264_21 = 21,
|
||||
H264_22 = 22,
|
||||
H264_3 = 30,
|
||||
H264_31 = 31,
|
||||
H264_32 = 32,
|
||||
H264_4 = 40,
|
||||
H264_41 = 41,
|
||||
H264_42 = 42,
|
||||
H264_5 = 50,
|
||||
H264_51 = 51,
|
||||
H264_52 = 52,
|
||||
H264_60 = 60,
|
||||
H264_61 = 61,
|
||||
H264_62 = 62,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum HEVCLevel {
|
||||
HEVC1 = 30,
|
||||
HEVC2 = 60,
|
||||
HEVC21 = 63,
|
||||
HEVC3 = 90,
|
||||
HEVC31 = 93,
|
||||
HEVC4 = 120,
|
||||
HEVC41 = 123,
|
||||
HEVC5 = 150,
|
||||
HEVC51 = 153,
|
||||
HEVC52 = 156,
|
||||
HEVC6 = 180,
|
||||
HEVC61 = 183,
|
||||
HEVC62 = 186,
|
||||
HEVCMain = 0,
|
||||
HEVCHigh = 1,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum AV1Level {
|
||||
// Also AV1_0
|
||||
AV1_2 = 0,
|
||||
// Also AV1_1
|
||||
AV1_21 = 1,
|
||||
AV1_22 = 2,
|
||||
AV1_23 = 3,
|
||||
AV1_3 = 4,
|
||||
AV1_31 = 5,
|
||||
AV1_32 = 6,
|
||||
AV1_33 = 7,
|
||||
AV1_4 = 8,
|
||||
AV1_41 = 9,
|
||||
AV1_42 = 10,
|
||||
AV1_43 = 11,
|
||||
AV1_5 = 12,
|
||||
AV1_51 = 13,
|
||||
AV1_52 = 14,
|
||||
AV1_53 = 15,
|
||||
AV1_6 = 16,
|
||||
AV1_61 = 17,
|
||||
AV1_62 = 18,
|
||||
AV1_63 = 19,
|
||||
AV1_7 = 20,
|
||||
AV1_71 = 21,
|
||||
AV1_72 = 22,
|
||||
AV1_73 = 23,
|
||||
AV1AutoSelect = 24,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencPicFlags {
|
||||
ForceIntra = 0x1,
|
||||
ForceIDR = 0x2,
|
||||
OutputSpspps = 0x4,
|
||||
Eos = 0x8,
|
||||
DisableEncStateAdvance = 0x10,
|
||||
OuputReconFrame = 0x20,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencMemoryHeap {
|
||||
AutoSelect = 0,
|
||||
VID = 1,
|
||||
SystemCached = 2,
|
||||
SystemUncached = 3,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencBFrameRefMode {
|
||||
Disabled = 0x0,
|
||||
Each = 0x1,
|
||||
Middle = 0x2,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencH264EntropyCodingMode {
|
||||
AutoSelect = 0x0,
|
||||
Cabac = 0x1,
|
||||
Cavlc = 0x2,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencH264BDirectMode {
|
||||
Autoselect = 0x0,
|
||||
Disable = 0x1,
|
||||
Temporal = 0x2,
|
||||
Spatial = 0x3,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencH264FMOMode {
|
||||
AutoSelect = 0x0,
|
||||
Enable = 0x1,
|
||||
Disable = 0x2,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencH264AdaptiveTransformMode {
|
||||
AutoSelect = 0x0,
|
||||
Disable = 0x1,
|
||||
Enable = 0x2,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencStereoPackingMode {
|
||||
None = 0x0,
|
||||
Checkerboard = 0x1,
|
||||
ColInterleave = 0x2,
|
||||
RowInterleave = 0x3,
|
||||
SideBySide = 0x4,
|
||||
TopBottm = 0x5,
|
||||
FrameSeq = 0x6,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencInputResourceType {
|
||||
DirectX = 0x0,
|
||||
CudaDeivcePtr = 0x1,
|
||||
CudaArray = 0x2,
|
||||
OpenGLTex = 0x3,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencBufferUsage {
|
||||
Image = 0x0,
|
||||
MotionVector = 0x1,
|
||||
BitStream = 0x2,
|
||||
OutputRecon = 0x4,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone)]
|
||||
pub enum NVencDeviceType {
|
||||
DirectX = 0x0,
|
||||
Cuda = 0x1,
|
||||
OpenGL = 0x2,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencNumRefFrames {
|
||||
AutoSelect = 0x0,
|
||||
Frames1 = 0x1,
|
||||
Frames2 = 0x2,
|
||||
Frames3 = 0x3,
|
||||
Frames4 = 0x4,
|
||||
Frames5 = 0x5,
|
||||
Frames6 = 0x6,
|
||||
Frames7 = 0x7,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencTemporalFilterLevel {
|
||||
Level0 = 0,
|
||||
Level4 = 4,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencCaps {
|
||||
NumMaxBFrames,
|
||||
SupportedRateControlModes,
|
||||
FieldEncoding,
|
||||
MonoChrome,
|
||||
FMO,
|
||||
QPELMV,
|
||||
BDirectMode,
|
||||
CABAC,
|
||||
AdaptiveTransform,
|
||||
StereoMVC,
|
||||
MaxTemporalLayers,
|
||||
HierarchicalBFrames,
|
||||
LevelMax,
|
||||
LevelMin,
|
||||
SeperateColorPlane,
|
||||
WidthMax,
|
||||
HeightMax,
|
||||
TemporalSVC,
|
||||
DynResChnage,
|
||||
DynBitrateChange,
|
||||
DynForceConstQP,
|
||||
DynRcModeChange,
|
||||
SubframeReadBack,
|
||||
ConstrainedEncoding,
|
||||
IntraRefresh,
|
||||
CustomVBVBufSize,
|
||||
DynamicSliceMode,
|
||||
Invalidation,
|
||||
PreProcSupport,
|
||||
AsyncEncodeSupport,
|
||||
MbNumMax,
|
||||
MbPerSecMax,
|
||||
YUV444Encode,
|
||||
LoselessEncode,
|
||||
SupportSao,
|
||||
MeOnlyMode,
|
||||
LookAhead,
|
||||
TemporalAQ,
|
||||
MaxLtrFrames,
|
||||
SupportWeightedPrediction,
|
||||
DynamicQueryEncoderCapacity,
|
||||
BFrameRefMode,
|
||||
EmphasisLevelMap,
|
||||
WidthMin,
|
||||
HeightMin,
|
||||
MultipleRefFrames,
|
||||
AlphaLayerEncoding,
|
||||
NumEncoderEngines,
|
||||
SingleSliceIntraRefresh,
|
||||
DisableEncStateAdvance,
|
||||
OutputReconSurface,
|
||||
OutputBlockStats,
|
||||
SupportTemporalFilter,
|
||||
LookAheadLevel,
|
||||
UndirectionalB,
|
||||
MVHEVCEncode,
|
||||
SupportYUV422Encode,
|
||||
ExposedCount,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencHEVCCuSize {
|
||||
AutoSelect = 0,
|
||||
Size8x8 = 1,
|
||||
Size16x16 = 2,
|
||||
Size32x32 = 3,
|
||||
Size64x64 = 4,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencAV1PartSize {
|
||||
AutoSelect = 0,
|
||||
Size4x4 = 1,
|
||||
Size8x8 = 2,
|
||||
Size16x16 = 3,
|
||||
Size32x32 = 4,
|
||||
Size64x64 = 5,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencVUIVideoFormat {
|
||||
Component = 0,
|
||||
Pal = 1,
|
||||
NTSC = 2,
|
||||
SeCam = 3,
|
||||
Mac = 4,
|
||||
Unspecified = 5,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencVUIColorPrimaries {
|
||||
Undefined = 0,
|
||||
BT709 = 1,
|
||||
Unspecified = 2,
|
||||
Reserved = 3,
|
||||
BT470M = 4,
|
||||
BT470BG = 5,
|
||||
SMPTE170M = 6,
|
||||
SMPTE240M = 7,
|
||||
FIlm = 8,
|
||||
BT2020 = 9,
|
||||
SMPTE428 = 10,
|
||||
SMPTE431 = 11,
|
||||
SMPTE432 = 12,
|
||||
JEDECP22 = 22,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencVUITransCharacteristics {
|
||||
Undefined = 0,
|
||||
BT709 = 1,
|
||||
Unspecified = 2,
|
||||
Reserved = 3,
|
||||
BT470M = 4,
|
||||
BT470BG = 5,
|
||||
SMPTE170M = 6,
|
||||
SMPTE240M = 7,
|
||||
Linear = 8,
|
||||
Log = 9,
|
||||
LogSqrt = 10,
|
||||
IEC61966_2_4 = 11,
|
||||
SRGB = 13,
|
||||
BT2020_10 = 14,
|
||||
BT2020_12 = 15,
|
||||
SMPTE2084 = 16,
|
||||
SMPTE428 = 17,
|
||||
AribStdB67 = 18,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencVUIMatrixCoeffs {
|
||||
RGB = 0,
|
||||
BT709 = 1,
|
||||
Unspecified = 2,
|
||||
Reserved = 3,
|
||||
FCC = 4,
|
||||
BT470BG = 5,
|
||||
SMPTE170M,
|
||||
SMPTE240M,
|
||||
YCGCO = 8,
|
||||
BT2020NCL = 9,
|
||||
BT2020CL = 10,
|
||||
SMPTE2085 = 11,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencLookAheadLevel {
|
||||
Level0 = 0,
|
||||
Level1 = 1,
|
||||
Level2 = 2,
|
||||
Level3 = 3,
|
||||
AutoSelect = 15,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencBitDepth {
|
||||
Invalid = 0,
|
||||
Depth8 = 8,
|
||||
Depth10 = 10,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C)]
|
||||
pub enum NVencTuningInfo {
|
||||
Undefined = 0,
|
||||
HighQuality = 1,
|
||||
LowLatency = 2,
|
||||
UltraLowLatency = 3,
|
||||
Lossless = 4,
|
||||
UltraHighQuality = 5,
|
||||
Count,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub enum NVencSplitEncodeMode {
|
||||
Audo = 0,
|
||||
AutoForced = 1,
|
||||
TwoForced = 2,
|
||||
ThreeForced = 3,
|
||||
FourForced = 4,
|
||||
Disable = 15,
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
use std::{ffi::c_char, mem::MaybeUninit, os::raw::c_void};
|
||||
|
||||
use crate::{
|
||||
stdcall,
|
||||
sys::{
|
||||
enums::{NVencBufferFormat, NVencTuningInfo},
|
||||
result::NVencResult,
|
||||
structs::{
|
||||
Guid, NVencCapsParam, NVencCreateBitstreamBuffer, NVencCreateInputBuffer,
|
||||
NVencInitializeParams, NVencLockBitStream, NVencLockInputBuffer, NVencMapInputResource,
|
||||
NVencOpenEncodeSessionExParams, NVencPicParams, NVencPresetConfig,
|
||||
NVencRegisterResource,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Clone)]
|
||||
pub struct NVencFunctionList {
|
||||
pub version: u32,
|
||||
rsvd: u32,
|
||||
#[deprecated]
|
||||
nvenc_open_encode_session: NvencOpenEncodeSession,
|
||||
pub nvenc_get_encoder_guid_count: NvencGetEncodeGuidCount,
|
||||
nvenc_get_encoder_profile_guid_count: NvencGetEncodeProfileGuidCount,
|
||||
nvenc_get_encoder_profile_guids: NvencGetEncodeProfileGuids,
|
||||
pub nvenc_get_encoder_guids: NvencGetEncodeGuids,
|
||||
nvenc_get_input_format_count: NvencGetInputFormatCount,
|
||||
nvenc_get_input_formats: NvencGetInputFormats,
|
||||
nvenc_get_encode_caps: NvencGetEncodeCaps,
|
||||
pub nvenc_get_encode_preset_count: NvencGetEncodePresetCount,
|
||||
pub nvenc_get_encode_preset_guids: NvencGetEncodePresetGuids,
|
||||
nvenc_get_encode_preset_config: NvencGetEncodePresetConfig,
|
||||
pub nvenc_initialize_encoder: NvencInitializeEncoder,
|
||||
pub nvenc_create_input_buffer: NvencCreateInputBuffer,
|
||||
pub nvenc_destroy_input_buffer: NvencDestroyInputBuffer,
|
||||
pub nvenc_create_bit_stream_buffer: NvencCreateBitStreamBuffer,
|
||||
pub nvenc_destory_bit_stream_buffer: NvencDestroyBitStreamBuffer,
|
||||
pub nvenc_encode_picture: NvencEncodePicture,
|
||||
pub nvenc_lock_bit_stream: NvencLockBitStream,
|
||||
pub nvenc_unlock_bit_stream: NvencUnlockBitStream,
|
||||
pub nvenc_lock_input_buffer: NvencLockInputBuffer,
|
||||
pub nvenc_unlock_input_buffer: NvencUnlockInputBuffer,
|
||||
nvenc_get_encode_stats: NvencGetEncodeStats,
|
||||
nvenc_get_sequence_params: NvencGetSequenceParams,
|
||||
nvenc_register_async_event: NvencRegisterAsyncEvent,
|
||||
nvenc_unregister_async_event: NvencUnregisterAsyncEvent,
|
||||
pub nvenc_map_input_resource: NvencMapInputResource,
|
||||
pub nvenc_unmap_input_resource: NvencUnmapInputResource,
|
||||
pub nvenc_destroy_encoder: NvencDestroyEncoder,
|
||||
nvenc_invalidate_ref_frames: NvencInvalidateRefFrames,
|
||||
pub nvenc_open_encode_session_ex: NvencOpenEncodeSessionEx,
|
||||
pub nvenc_register_resource: NvencRegisterResource,
|
||||
pub nvenc_unregister_resource: NvencUnRegisterResource,
|
||||
nvenc_reconfigure_encoder: NvencReconfigureEncoder,
|
||||
rsvd1: *mut c_void,
|
||||
nvenc_create_mv_buffer: NvencCreateMVBuffer,
|
||||
nvenc_destory_mv_buffer: NvencDestoryMVBuffer,
|
||||
nvenc_run_motion_estimation_only: NvencRunMotionEstimationOnly,
|
||||
pub nvenc_get_last_error: NvencGetLastError,
|
||||
nvenc_set_io_cuda_streams: NvencSetIOCudaStreams,
|
||||
pub nvenc_get_encode_preset_config_ex: NvencGetEncodePresetConfigEx,
|
||||
nvenc_get_sequence_params_ex: NvencGetSequenceParamsEx,
|
||||
nvenc_store_encoder_state: NvencStoreEncoderState,
|
||||
nvenc_look_ahead_picture: NvencLookAheadPicture,
|
||||
rsvd2: [*mut c_void; 275],
|
||||
}
|
||||
|
||||
type NvencOpenEncodeSession =
|
||||
stdcall!(fn(device: *mut c_void, device_type: i32, encoder: *mut *mut c_void) -> NVencResult);
|
||||
type NvencGetEncodeGuidCount =
|
||||
stdcall!(fn(encoder: *mut c_void, encoder_guid_count: *mut u32) -> NVencResult);
|
||||
type NvencGetEncodeGuids = stdcall!(
|
||||
fn(
|
||||
encoder: *mut c_void,
|
||||
guids: *mut Guid,
|
||||
guid_array_size: u32,
|
||||
guid_count: *mut u32,
|
||||
) -> NVencResult
|
||||
);
|
||||
type NvencGetEncodeProfileGuidCount = stdcall!(
|
||||
fn(
|
||||
encoder: *mut c_void,
|
||||
encode_guid: Guid,
|
||||
encoder_profile_guid_count: *mut u32,
|
||||
) -> NVencResult
|
||||
);
|
||||
type NvencGetEncodeProfileGuids = stdcall!(
|
||||
fn(
|
||||
encoder: *mut c_void,
|
||||
profile_guids: *mut Guid,
|
||||
guid_array_size: u32,
|
||||
guid_count: *mut u32,
|
||||
) -> NVencResult
|
||||
);
|
||||
type NvencGetInputFormatCount = stdcall!(
|
||||
fn(encoder: *mut c_void, encoder_guid: Guid, input_fmt_count: *mut u32) -> NVencResult
|
||||
);
|
||||
type NvencGetInputFormats = stdcall!(
|
||||
fn(
|
||||
encoder: *mut c_void,
|
||||
input_formats: *mut NVencBufferFormat,
|
||||
input_format_array_size: u32,
|
||||
input_format_count: *mut u32,
|
||||
) -> NVencResult
|
||||
);
|
||||
type NvencGetEncodeCaps = stdcall!(
|
||||
fn(
|
||||
encoder: *mut c_void,
|
||||
encoder_guid: Guid,
|
||||
caps_param: *mut NVencCapsParam,
|
||||
caps_val: *mut i32,
|
||||
) -> NVencResult
|
||||
);
|
||||
type NvencGetEncodePresetCount = stdcall!(
|
||||
fn(
|
||||
encoder: *mut c_void,
|
||||
encoder_guid: Guid,
|
||||
encoder_preset_guid_count: *mut u32,
|
||||
) -> NVencResult
|
||||
);
|
||||
type NvencGetEncodePresetGuids = stdcall!(
|
||||
fn(
|
||||
encoder: *mut c_void,
|
||||
encode_guid: Guid,
|
||||
preset_guids: *mut Guid,
|
||||
guid_array_size: u32,
|
||||
encoder_preset_guid_count: *mut u32,
|
||||
) -> NVencResult
|
||||
);
|
||||
type NvencGetEncodePresetConfig = stdcall!(
|
||||
fn(
|
||||
encoder: *mut c_void,
|
||||
encode_guid: Guid,
|
||||
preset_guid: Guid,
|
||||
preset_config: *mut NVencPresetConfig,
|
||||
) -> NVencResult
|
||||
);
|
||||
type NvencGetEncodePresetConfigEx = stdcall!(
|
||||
fn(
|
||||
encoder: *mut c_void,
|
||||
encode_guid: Guid,
|
||||
preset_guid: Guid,
|
||||
tuning_info: NVencTuningInfo,
|
||||
preset_config: *mut MaybeUninit<NVencPresetConfig>,
|
||||
) -> NVencResult
|
||||
);
|
||||
type NvencInitializeEncoder = stdcall!(
|
||||
fn(encoder: *mut c_void, create_encode_params: *mut NVencInitializeParams) -> NVencResult
|
||||
);
|
||||
type NvencCreateInputBuffer = stdcall!(
|
||||
fn(
|
||||
encoder: *mut c_void,
|
||||
create_input_buffer_params: *mut NVencCreateInputBuffer,
|
||||
) -> NVencResult
|
||||
);
|
||||
type NvencDestroyInputBuffer =
|
||||
stdcall!(fn(encoder: *mut c_void, input_buffer: *mut c_void) -> NVencResult);
|
||||
type NvencCreateBitStreamBuffer = stdcall!(
|
||||
fn(
|
||||
encoder: *mut c_void,
|
||||
create_bit_stream_buffer_params: *mut NVencCreateBitstreamBuffer,
|
||||
) -> NVencResult
|
||||
);
|
||||
type NvencDestroyBitStreamBuffer =
|
||||
stdcall!(fn(encoder: *mut c_void, bitstream_buffer: *mut c_void) -> NVencResult);
|
||||
type NvencEncodePicture = stdcall!(
|
||||
fn(encoder: *mut c_void, encode_pic_params: *mut MaybeUninit<NVencPicParams>) -> NVencResult
|
||||
);
|
||||
type NvencLockBitStream = stdcall!(
|
||||
fn(
|
||||
encoder: *mut c_void,
|
||||
lock_bitstream_buffer_params: *mut MaybeUninit<NVencLockBitStream>,
|
||||
) -> NVencResult
|
||||
);
|
||||
type NvencUnlockBitStream =
|
||||
stdcall!(fn(encoder: *mut c_void, bitstream_buffer: *mut c_void) -> NVencResult);
|
||||
type NvencLockInputBuffer = stdcall!(
|
||||
fn(encoder: *mut c_void, lock_input_buffer_params: *mut NVencLockInputBuffer) -> NVencResult
|
||||
);
|
||||
type NvencUnlockInputBuffer =
|
||||
stdcall!(fn(encoder: *mut c_void, input_buffer: *mut c_void) -> NVencResult);
|
||||
type NvencGetEncodeStats = stdcall!(fn(encoder: *mut c_void) -> NVencResult);
|
||||
type NvencGetSequenceParams = stdcall!(fn(encoder: *mut c_void) -> NVencResult);
|
||||
type NvencRegisterAsyncEvent = stdcall!(fn(encoder: *mut c_void) -> NVencResult);
|
||||
type NvencUnregisterAsyncEvent = stdcall!(fn(encoder: *mut c_void) -> NVencResult);
|
||||
type NvencMapInputResource = stdcall!(
|
||||
fn(encoder: *mut c_void, map_input_resource_params: *mut NVencMapInputResource) -> NVencResult
|
||||
);
|
||||
type NvencUnmapInputResource =
|
||||
stdcall!(fn(encoder: *mut c_void, mapped_input_buffer: *mut c_void) -> NVencResult);
|
||||
type NvencDestroyEncoder = stdcall!(fn(encoder: *mut c_void) -> NVencResult);
|
||||
type NvencInvalidateRefFrames = stdcall!(fn(encoder: *mut c_void) -> NVencResult);
|
||||
type NvencOpenEncodeSessionEx = stdcall!(
|
||||
fn(
|
||||
open_session_ex_params: *mut NVencOpenEncodeSessionExParams,
|
||||
encoder: *mut *mut c_void,
|
||||
) -> NVencResult
|
||||
);
|
||||
type NvencRegisterResource = stdcall!(
|
||||
fn(encoder: *mut c_void, register_res_params: *mut NVencRegisterResource) -> NVencResult
|
||||
);
|
||||
type NvencUnRegisterResource =
|
||||
stdcall!(fn(encoder: *mut c_void, registered_res: *mut c_void) -> NVencResult);
|
||||
type NvencReconfigureEncoder = stdcall!(fn(encoder: *mut c_void) -> NVencResult);
|
||||
|
||||
type NvencCreateMVBuffer = stdcall!(fn(encoder: *mut c_void) -> NVencResult);
|
||||
type NvencDestoryMVBuffer = stdcall!(fn(encoder: *mut c_void) -> NVencResult);
|
||||
type NvencRunMotionEstimationOnly = stdcall!(fn(encoder: *mut c_void) -> NVencResult);
|
||||
type NvencGetLastError = stdcall!(fn(encoder: *mut c_void) -> *const c_char);
|
||||
type NvencSetIOCudaStreams = stdcall!(fn(encoder: *mut c_void) -> NVencResult);
|
||||
type NvencGetSequenceParamsEx = stdcall!(fn(encoder: *mut c_void) -> NVencResult);
|
||||
type NvencStoreEncoderState = stdcall!(fn(encoder: *mut c_void) -> NVencResult);
|
||||
type NvencLookAheadPicture = stdcall!(fn(encoder: *mut c_void) -> NVencResult);
|
||||
Vendored
+146
@@ -0,0 +1,146 @@
|
||||
use crate::sys::structs::Guid;
|
||||
|
||||
pub const NV_ENC_CODEC_H264_GUID: Guid = Guid::from_values(
|
||||
0x6bc82762,
|
||||
0x4e63,
|
||||
0x4ca4,
|
||||
[0xaa, 0x85, 0x1e, 0x50, 0xf3, 0x21, 0xf6, 0xbf],
|
||||
);
|
||||
pub const NV_ENC_CODEC_HEVC_GUID: Guid = Guid::from_values(
|
||||
0x790cdc88,
|
||||
0x4522,
|
||||
0x4d7b,
|
||||
[0x94, 0x25, 0xbd, 0xa9, 0x97, 0x5f, 0x76, 0x3],
|
||||
);
|
||||
pub const NV_ENC_CODEC_AV1_GUID: Guid = Guid::from_values(
|
||||
0x0a352289,
|
||||
0x0aa7,
|
||||
0x4759,
|
||||
[0x86, 0x2d, 0x5d, 0x15, 0xcd, 0x16, 0xd2, 0x54],
|
||||
);
|
||||
pub const NV_ENC_CODEC_PROFILE_AUTOSELECT_GUID: Guid = Guid::from_values(
|
||||
0xbfd6f8e7,
|
||||
0x233c,
|
||||
0x4341,
|
||||
[0x8b, 0x3e, 0x48, 0x18, 0x52, 0x38, 0x3, 0xf4],
|
||||
);
|
||||
pub const NV_ENC_H264_PROFILE_BASELINE_GUID: Guid = Guid::from_values(
|
||||
0x727bcaa,
|
||||
0x78c4,
|
||||
0x4c83,
|
||||
[0x8c, 0x2f, 0xef, 0x3d, 0xff, 0x26, 0x7c, 0x6a],
|
||||
);
|
||||
pub const NV_ENC_H264_PROFILE_MAIN_GUID: Guid = Guid::from_values(
|
||||
0x60b5c1d4,
|
||||
0x67fe,
|
||||
0x4790,
|
||||
[0x94, 0xd5, 0xc4, 0x72, 0x6d, 0x7b, 0x6e, 0x6d],
|
||||
);
|
||||
pub const NV_ENC_H264_PROFILE_HIGH_GUID: Guid = Guid::from_values(
|
||||
0xe7cbc309,
|
||||
0x4f7a,
|
||||
0x4b89,
|
||||
[0xaf, 0x2a, 0xd5, 0x37, 0xc9, 0x2b, 0xe3, 0x10],
|
||||
);
|
||||
pub const NV_ENC_H264_PROFILE_HIGH_10_GUID: Guid = Guid::from_values(
|
||||
0x8f0c337e,
|
||||
0x186c,
|
||||
0x48e9,
|
||||
[0xa6, 0x9d, 0x7a, 0x83, 0x34, 0x08, 0x97, 0x5],
|
||||
);
|
||||
pub const NV_ENC_H264_PROFILE_HIGH_422_GUID: Guid = Guid::from_values(
|
||||
0xff3242e9,
|
||||
0x613c,
|
||||
0x4295,
|
||||
[0xa1, 0xe8, 0x2a, 0x7f, 0xe9, 0x4d, 0x81, 0x33],
|
||||
);
|
||||
pub const NV_ENC_H264_PROFILE_HIGH_444_GUID: Guid = Guid::from_values(
|
||||
0x7ac663cb,
|
||||
0xa598,
|
||||
0x4960,
|
||||
[0xb8, 0x44, 0x33, 0x9b, 0x26, 0x1a, 0x7d, 0x52],
|
||||
);
|
||||
pub const NV_ENC_H264_PROFILE_STEREO_GUID: Guid = Guid::from_values(
|
||||
0x40847bf5,
|
||||
0x33f7,
|
||||
0x4601,
|
||||
[0x90, 0x84, 0xe8, 0xfe, 0x3c, 0x1d, 0xb8, 0xb7],
|
||||
);
|
||||
pub const NV_ENC_H264_PROFILE_PROGRESSIVE_HIGH_GUID: Guid = Guid::from_values(
|
||||
0xb405afac,
|
||||
0xf32b,
|
||||
0x417b,
|
||||
[0x89, 0xc4, 0x9a, 0xbe, 0xed, 0x3e, 0x59, 0x78],
|
||||
);
|
||||
pub const NV_ENC_H264_PROFILE_CONSTRAINED_HIGH_GUID: Guid = Guid::from_values(
|
||||
0xaec1bd87,
|
||||
0xe85b,
|
||||
0x48f2,
|
||||
[0x84, 0xc3, 0x98, 0xbc, 0xa6, 0x28, 0x50, 0x72],
|
||||
);
|
||||
pub const NV_ENC_HEVC_PROFILE_MAIN_GUID: Guid = Guid::from_values(
|
||||
0xb514c39a,
|
||||
0xb55b,
|
||||
0x40fa,
|
||||
[0x87, 0x8f, 0xf1, 0x25, 0x3b, 0x4d, 0xfd, 0xec],
|
||||
);
|
||||
pub const NV_ENC_HEVC_PROFILE_MAIN10_GUID: Guid = Guid::from_values(
|
||||
0xfa4d2b6c,
|
||||
0x3a5b,
|
||||
0x411a,
|
||||
[0x80, 0x18, 0x0a, 0x3f, 0x5e, 0x3c, 0x9b, 0xe5],
|
||||
);
|
||||
pub const NV_ENC_HEVC_PROFILE_FREXT_GUID: Guid = Guid::from_values(
|
||||
0x51ec32b5,
|
||||
0x1b4c,
|
||||
0x453c,
|
||||
[0x9c, 0xbd, 0xb6, 0x16, 0xbd, 0x62, 0x13, 0x41],
|
||||
);
|
||||
pub const NV_ENC_AV1_PROFILE_MAIN_GUID: Guid = Guid::from_values(
|
||||
0x5f2a39f5,
|
||||
0xf14e,
|
||||
0x4f95,
|
||||
[0x9a, 0x9e, 0xb7, 0x6d, 0x56, 0x8f, 0xcf, 0x97],
|
||||
);
|
||||
pub const NV_ENC_PRESET_P1_GUID: Guid = Guid::from_values(
|
||||
0xfc0a8d3e,
|
||||
0x45f8,
|
||||
0x4cf8,
|
||||
[0x80, 0xc7, 0x29, 0x88, 0x71, 0x59, 0xe, 0xbf],
|
||||
);
|
||||
pub const NV_ENC_PRESET_P2_GUID: Guid = Guid::from_values(
|
||||
0xf581cfb8,
|
||||
0x88d6,
|
||||
0x4381,
|
||||
[0x93, 0xf0, 0xdf, 0x13, 0xf9, 0xc2, 0x7d, 0xab],
|
||||
);
|
||||
pub const NV_ENC_PRESET_P3_GUID: Guid = Guid::from_values(
|
||||
0x36850110,
|
||||
0x3a07,
|
||||
0x441f,
|
||||
[0x94, 0xd5, 0x36, 0x70, 0x63, 0x1f, 0x91, 0xf6],
|
||||
);
|
||||
pub const NV_ENC_PRESET_P4_GUID: Guid = Guid::from_values(
|
||||
0x90a7b826,
|
||||
0xdf06,
|
||||
0x4862,
|
||||
[0xb9, 0xd2, 0xcd, 0x6d, 0x73, 0xa0, 0x86, 0x81],
|
||||
);
|
||||
pub const NV_ENC_PRESET_P5_GUID: Guid = Guid::from_values(
|
||||
0x21c6e6b4,
|
||||
0x297a,
|
||||
0x4cba,
|
||||
[0x99, 0x8f, 0xb6, 0xcb, 0xde, 0x72, 0xad, 0xe3],
|
||||
);
|
||||
pub const NV_ENC_PRESET_P6_GUID: Guid = Guid::from_values(
|
||||
0x8e75c279,
|
||||
0x6299,
|
||||
0x4ab6,
|
||||
[0x83, 0x2, 0xb, 0x21, 0x5a, 0x33, 0x5c, 0xf5],
|
||||
);
|
||||
pub const NV_ENC_PRESET_P7_GUID: Guid = Guid::from_values(
|
||||
0x84848c12,
|
||||
0x6f71,
|
||||
0x4c13,
|
||||
[0x93, 0x1b, 0x53, 0xe2, 0x83, 0xf5, 0x79, 0x74],
|
||||
);
|
||||
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
pub mod enums;
|
||||
pub mod function_table;
|
||||
pub mod guids;
|
||||
pub mod result;
|
||||
pub mod structs;
|
||||
pub mod version;
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub enum NVencResult {
|
||||
Success,
|
||||
NoDevice,
|
||||
UnsupportedDevice,
|
||||
InvalidEncoderDevice,
|
||||
InvalidDevice,
|
||||
DeviceNotExist,
|
||||
InvalidPtr,
|
||||
InvalidParam,
|
||||
InvalidEvent,
|
||||
InvalidCall,
|
||||
OutOfMemory,
|
||||
NotInitialized,
|
||||
UnsupportedParam,
|
||||
LockBusy,
|
||||
NotEnoughBuffer,
|
||||
InvalidVersion,
|
||||
MapFailed,
|
||||
NeedMoreInput,
|
||||
EncoderBusy,
|
||||
EventNotRegisterd,
|
||||
Generic,
|
||||
IncompatibleClientKey,
|
||||
Unimplemented,
|
||||
ResourceRegisterFailed,
|
||||
ResourceNotRegistered,
|
||||
ResourceNotMapped,
|
||||
NeedMoreOutput,
|
||||
}
|
||||
|
||||
impl NVencResult {
|
||||
pub fn into_error(self) -> Result<(), NVencError> {
|
||||
NVencError::from_result(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl NVencError {
|
||||
pub fn from_result(value: NVencResult) -> Result<(), Self> {
|
||||
if value == NVencResult::Success {
|
||||
Ok(())
|
||||
} else {
|
||||
// Safety: They are one case different, so they always align
|
||||
unsafe { Err(std::mem::transmute::<NVencResult, NVencError>(value)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub enum NVencError {
|
||||
NoDevice = NVencResult::NoDevice as isize,
|
||||
UnsupportedDevice,
|
||||
InvalidEncoderDevice,
|
||||
InvalidDevice,
|
||||
DeviceNotExist,
|
||||
InvalidPtr,
|
||||
InvalidEvent,
|
||||
InvalidParam,
|
||||
InvalidCall,
|
||||
OutOfMemory,
|
||||
NotInitialized,
|
||||
UnsupportedParam,
|
||||
LockBusy,
|
||||
NotEnoughBuffer,
|
||||
InvalidVersion,
|
||||
MapFailed,
|
||||
NeedMoreInput,
|
||||
EncoderBusy,
|
||||
EventNotRegisterd,
|
||||
Generic,
|
||||
IncompatibleClientKey,
|
||||
Unimplemented,
|
||||
ResourceRegisterFailed,
|
||||
ResourceNotRegistered,
|
||||
ResourceNotMapped,
|
||||
NeedMoreOutput,
|
||||
}
|
||||
Vendored
+944
@@ -0,0 +1,944 @@
|
||||
use std::{ffi::c_void, mem::ManuallyDrop};
|
||||
|
||||
use crate::sys::{
|
||||
enums::{
|
||||
NVencAV1PartSize, NVencBFrameRefMode, NVencBitDepth, NVencBufferFormat, NVencBufferUsage,
|
||||
NVencCaps, NVencDeviceType, NVencDisplayPicStruct, NVencH264AdaptiveTransformMode,
|
||||
NVencH264BDirectMode, NVencH264EntropyCodingMode, NVencH264FMOMode, NVencHEVCCuSize,
|
||||
NVencInputResourceType, NVencLookAheadLevel, NVencMVPrecision, NVencMemoryHeap,
|
||||
NVencMultiPass, NVencNumRefFrames, NVencOutputStatsLevel, NVencParamsFrameFieldMode,
|
||||
NVencParamsRcMode, NVencPicStruct, NVencPicType, NVencQPMapMode, NVencStateRestoreType,
|
||||
NVencStereoPackingMode, NVencTemporalFilterLevel, NVencTuningInfo, NVencVUIColorPrimaries,
|
||||
NVencVUIMatrixCoeffs, NVencVUITransCharacteristics, NVencVUIVideoFormat,
|
||||
},
|
||||
version::struct_version,
|
||||
};
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Eq)]
|
||||
#[repr(C)]
|
||||
pub struct Guid {
|
||||
pub data1: u32,
|
||||
pub data2: u16,
|
||||
pub data3: u16,
|
||||
pub data4: [u8; 8],
|
||||
}
|
||||
|
||||
impl Guid {
|
||||
pub const fn from_values(data1: u32, data2: u16, data3: u16, data4: [u8; 8]) -> Self {
|
||||
Self {
|
||||
data1,
|
||||
data2,
|
||||
data3,
|
||||
data4,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_u128(uuid: u128) -> Self {
|
||||
Self {
|
||||
data1: (uuid >> 96) as u32,
|
||||
data2: ((uuid >> 80) & 0xffff) as u16,
|
||||
data3: ((uuid >> 64) & 0xffff) as u16,
|
||||
data4: (uuid as u64).to_be_bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencCapsParam {
|
||||
version: u32,
|
||||
caps_to_query: NVencCaps,
|
||||
rsvd: [u32; 62],
|
||||
}
|
||||
|
||||
pub const NV_ENC_CAPS_PARAM_VERSION: u32 = struct_version(1);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencRestoreEncoderStateParams {
|
||||
version: u32,
|
||||
buffer: u32,
|
||||
state: NVencStateRestoreType,
|
||||
rsvd: u32,
|
||||
output_bit_stream: *mut c_void,
|
||||
completion_event: *mut c_void,
|
||||
rsvd1: [u32; 64],
|
||||
rsvd2: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
pub const NV_ENC_RESTORE_STATE_PARAMS_VER: u32 = struct_version(2);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencOutputStatsBlock {
|
||||
version: u32,
|
||||
qp: u8,
|
||||
rsvd: [u8; 3],
|
||||
bit_count: u32,
|
||||
satd_cost: u32,
|
||||
rsvd1: [u32; 12],
|
||||
}
|
||||
|
||||
pub const NV_ENC_OUTPUT_STATS_BLOCK_VER: u32 = struct_version(1);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencOutputStatsRow {
|
||||
version: u32,
|
||||
qp: u8,
|
||||
rsvd: [u8; 3],
|
||||
bit_count: u32,
|
||||
satd_cost: u32,
|
||||
rsvd1: [u32; 12],
|
||||
}
|
||||
|
||||
pub const NV_ENC_OUTPUT_STATS_ROW_VER: u32 = struct_version(1);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencEncodeOutParams {
|
||||
version: u32,
|
||||
bitstream_size_in_bytes: u32,
|
||||
rsvd: [u32; 62],
|
||||
}
|
||||
|
||||
pub const NV_ENC_ENCODE_OUT_PARAMS_VER: u32 = struct_version(1);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencLookAheadPicParams {
|
||||
version: u32,
|
||||
rsvd: u32,
|
||||
input_buffer: *mut c_void,
|
||||
picture_type: NVencPicType,
|
||||
rsvd1: [u32; 63],
|
||||
rsvd2: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
pub const NV_ENC_LOOKAHEAD_PIC_PARAMS_VER: u32 = struct_version(2);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencCreateInputBuffer {
|
||||
pub version: u32,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub memory_heap: NVencMemoryHeap,
|
||||
pub buffer_fmt: NVencBufferFormat,
|
||||
pub rsvd: u32,
|
||||
pub input_buffer: *mut c_void,
|
||||
pub p_sys_mem_buffer: *mut c_void,
|
||||
pub rsvd1: [u32; 58],
|
||||
pub rsvd2: [*mut c_void; 63],
|
||||
}
|
||||
|
||||
pub const NV_ENC_CREATE_INPUT_BUFFER_VER: u32 = struct_version(2);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencCreateBitstreamBuffer {
|
||||
pub version: u32,
|
||||
#[deprecated]
|
||||
size: u32,
|
||||
#[deprecated]
|
||||
memory_heap: NVencMemoryHeap,
|
||||
rsvd: u32,
|
||||
pub bitstream_buffer: *mut c_void,
|
||||
bitstream_buffer_ptr: *mut c_void,
|
||||
rsvd1: [u32; 58],
|
||||
rsvd2: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
pub const NV_ENC_CREATE_BITSTREAM_BUFFER_VER: u32 = struct_version(1);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencMVector {
|
||||
mvx: i16,
|
||||
mvy: i16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencH264MVData {
|
||||
mv: [NVencMVector; 4],
|
||||
mb_type: u8,
|
||||
partition_type: u8,
|
||||
rsvd: u16,
|
||||
mb_cost: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencHEVCMVData {
|
||||
mv: [NVencMVector; 4],
|
||||
cu_type: u8,
|
||||
cu_size: u8,
|
||||
partition_mode: u8,
|
||||
last_cu_int_ctb: u8,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencCreateMVBuffer {
|
||||
version: u32,
|
||||
rsvd: u32,
|
||||
mv_buffer: *mut c_void,
|
||||
rsvd1: [u32; 254],
|
||||
rsvd2: [*mut c_void; 63],
|
||||
}
|
||||
|
||||
pub const NV_ENC_CREATE_MV_BUFFER_VER: u32 = struct_version(2);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencQP {
|
||||
qp_inter_p: u32,
|
||||
qp_inter_b: u32,
|
||||
qp_intra: u32,
|
||||
}
|
||||
|
||||
pub const MAX_NUM_VIEWS_MINUS_1: usize = 7;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencRcParams {
|
||||
version: u32,
|
||||
pub rate_control_mode: NVencParamsRcMode,
|
||||
const_qp: NVencQP,
|
||||
pub average_bit_rate: u32,
|
||||
max_bit_rate: u32,
|
||||
vbv_buffer_size: u32,
|
||||
vbv_initial_delay: u32,
|
||||
// TODO: bit fields
|
||||
bit_fields: NVencRcParamsBitFlags,
|
||||
min_qp: NVencQP,
|
||||
max_qp: NVencQP,
|
||||
initial_rcqp: NVencQP,
|
||||
temporal_layer_idx_mask: u32,
|
||||
temporal_layer_qp: [u8; 8],
|
||||
target_quality: u8,
|
||||
target_quality_lsb: u8,
|
||||
pub look_ahead_depth: u16,
|
||||
low_delay_key_frame_scale: u8,
|
||||
y_dc_qp_index_offset: i8,
|
||||
u_dc_qp_index_offset: i8,
|
||||
v_dc_qp_index_offset: i8,
|
||||
qp_map_mode: NVencQPMapMode,
|
||||
multi_pass: NVencMultiPass,
|
||||
alpha_layer_bitrate_ratio: u32,
|
||||
cb_qp_index_offset: i8,
|
||||
cr_qp_index_offset: i8,
|
||||
rsvd2: u16,
|
||||
lookahead_level: NVencLookAheadLevel,
|
||||
view_bitrate_ratios: [u8; MAX_NUM_VIEWS_MINUS_1],
|
||||
rsvd3: u8,
|
||||
rsvd1: u32,
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct NVencRcParamsBitFlags(u32);
|
||||
|
||||
impl NVencRcParamsBitFlags {}
|
||||
|
||||
pub const NV_ENC_RC_PARAMS_VER: u32 = struct_version(1);
|
||||
|
||||
pub const MAX_NUM_CLOCK_TS: usize = 3;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencClockTimeStampSet {
|
||||
pub bitflags: u32,
|
||||
pub time_offset: u32,
|
||||
}
|
||||
|
||||
impl NVencClockTimeStampSet {}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencTimeCode {
|
||||
pub display_pic_struct: NVencDisplayPicStruct,
|
||||
pub clock_timestamp: [NVencClockTimeStampSet; MAX_NUM_CLOCK_TS],
|
||||
pub skip_clock_timestamp_insertion: u32,
|
||||
}
|
||||
|
||||
pub const MULTIVIEW_MAX_NUM_REF_DISPLAY: usize = 32;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct HEVC3DReferenceDisplayInfo {
|
||||
// TODO
|
||||
pub bitflags: u32,
|
||||
pub prec_ref_display_width: i32,
|
||||
pub prec_ref_viewing_dist: i32,
|
||||
pub num_ref_displays_minus1: i32,
|
||||
pub left_view_id: [i32; MULTIVIEW_MAX_NUM_REF_DISPLAY],
|
||||
pub right_view_id: [i32; MULTIVIEW_MAX_NUM_REF_DISPLAY],
|
||||
pub exponent_ref_display_width: [i32; MULTIVIEW_MAX_NUM_REF_DISPLAY],
|
||||
pub mantissa_ref_display_width: [i32; MULTIVIEW_MAX_NUM_REF_DISPLAY],
|
||||
pub exponent_ref_viewing_distance: [i32; MULTIVIEW_MAX_NUM_REF_DISPLAY],
|
||||
pub mantissa_ref_viewing_distance: [i32; MULTIVIEW_MAX_NUM_REF_DISPLAY],
|
||||
pub num_sample_shift_plus512: [i32; MULTIVIEW_MAX_NUM_REF_DISPLAY],
|
||||
pub additional_shift_present_flag: [u8; MULTIVIEW_MAX_NUM_REF_DISPLAY],
|
||||
pub rsvd2: [u32; 4],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct ChromaPoints {
|
||||
x: u16,
|
||||
y: u16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct MasteringDisplayInfo {
|
||||
pub g: ChromaPoints,
|
||||
pub b: ChromaPoints,
|
||||
pub r: ChromaPoints,
|
||||
pub white_point: ChromaPoints,
|
||||
pub max_luma: u32,
|
||||
pub min_luma: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct ContentLightLevel {
|
||||
pub max_content_light_level: u16,
|
||||
pub max_pic_average_light_level: u16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencConfigH264VUIParameters {
|
||||
overscan_info_present_flag: u32,
|
||||
overscan_info: u32,
|
||||
video_signal_type_present_flag: u32,
|
||||
video_format: NVencVUIVideoFormat,
|
||||
video_full_range_flag: u32,
|
||||
color_description_present_flag: u32,
|
||||
color_primaries: NVencVUIColorPrimaries,
|
||||
transfer_characteristics: NVencVUITransCharacteristics,
|
||||
color_matrix: NVencVUIMatrixCoeffs,
|
||||
chroma_sample_location_flag: u32,
|
||||
chroma_sample_location_top: u32,
|
||||
chroma_sample_location_bot: u32,
|
||||
bitstream_restriction_flag: u32,
|
||||
timing_info_present_flag: u32,
|
||||
num_unit_in_ticks: u32,
|
||||
time_scale: u32,
|
||||
rsvd: [u32; 12],
|
||||
}
|
||||
|
||||
pub type NVencConfigHEVCVUIParamaters = NVencConfigH264VUIParameters;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Default)]
|
||||
pub struct NVencExternalMeHintCountsPerBlockType {
|
||||
pub bitflags: u32,
|
||||
pub rsvd1: [u32; 3],
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct NVencExternalMeHint(i32);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencExternalMeSbHint {
|
||||
flags1: i16,
|
||||
flags2: i16,
|
||||
flags3: i16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencConfigH264 {
|
||||
bitflags: u32,
|
||||
level: u32,
|
||||
idr_period: u32,
|
||||
seperate_color_plane_flag: u32,
|
||||
disable_deblocking_filter_idc: u32,
|
||||
num_termpoal_layers: u32,
|
||||
sps_id: u32,
|
||||
pps_id: u32,
|
||||
adaptive_transform_mode: NVencH264AdaptiveTransformMode,
|
||||
fmo_mode: NVencH264FMOMode,
|
||||
bdirect_mode: NVencH264BDirectMode,
|
||||
entropy_coding_mode: NVencH264EntropyCodingMode,
|
||||
stereo_mode: NVencStereoPackingMode,
|
||||
intra_refresh_period: u32,
|
||||
intra_refresh_count: u32,
|
||||
max_num_ref_frames: u32,
|
||||
slice_mode: u32,
|
||||
slice_mode_data: u32,
|
||||
h264_vui_parameters: NVencConfigH264VUIParameters,
|
||||
ltr_num_frames: u32,
|
||||
ltr_trust_mode: u32,
|
||||
chroma_format_idc: u32,
|
||||
max_termpoal_layers: u32,
|
||||
use_b_frames_as_ref: NVencBFrameRefMode,
|
||||
num_ref_l0: NVencNumRefFrames,
|
||||
num_ref_l1: NVencNumRefFrames,
|
||||
output_bit_depth: NVencBitDepth,
|
||||
input_bit_depth: NVencBitDepth,
|
||||
tf_level: NVencTemporalFilterLevel,
|
||||
rsvd1: [u32; 264],
|
||||
rsvd: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencConfigHEVC {
|
||||
level: u32,
|
||||
tier: u32,
|
||||
min_cu_size: NVencHEVCCuSize,
|
||||
max_cu_size: NVencHEVCCuSize,
|
||||
bitflags: u32,
|
||||
idr_period: u32,
|
||||
intra_refresh_cnt: u32,
|
||||
max_num_ref_frames_in_dpb: u32,
|
||||
ltr_num_frames: u32,
|
||||
vps_id: u32,
|
||||
sps_id: u32,
|
||||
pps_id: u32,
|
||||
slice_mode: u32,
|
||||
slice_mode_data: u32,
|
||||
max_temporal_layers_minus_1: u32,
|
||||
hevc_vui_paramters: NVencConfigHEVCVUIParamaters,
|
||||
ltr_trust_mode: u32,
|
||||
use_b_frames_as_ref: NVencBFrameRefMode,
|
||||
tf_level: NVencTemporalFilterLevel,
|
||||
disable_deblocking_filter_idc: u32,
|
||||
output_bit_depth: NVencBitDepth,
|
||||
input_bit_depth: NVencBitDepth,
|
||||
num_temporal_layers: u32,
|
||||
num_views: u32,
|
||||
rsvd1: [u32; 208],
|
||||
rsvd2: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
pub const NV_MAX_TILE_COLS_AV1: usize = 64;
|
||||
pub const NV_MAX_TILE_ROWS_AV1: usize = 64;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencFilmGrainParamsAV1 {
|
||||
bitfields: u32,
|
||||
point_y_value: [u8; 14],
|
||||
point_y_scaling: [u8; 14],
|
||||
point_cb_value: [u8; 10],
|
||||
point_cb_scaling: [u8; 10],
|
||||
point_cr_value: [u8; 10],
|
||||
point_cr_scaling: [u8; 10],
|
||||
ar_coeffs_y_plus128: [u8; 24],
|
||||
ar_coeffs_cb_plus128: [u8; 25],
|
||||
ar_coeffs_cr_plus128: [u8; 25],
|
||||
rsvd2: [u8; 2],
|
||||
cb_mult: u8,
|
||||
cb_luma_mult: u8,
|
||||
cb_offset: u16,
|
||||
cr_mult: u8,
|
||||
cr_luma_mult: u8,
|
||||
cr_offset: u16,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencConfigAV1 {
|
||||
level: u32,
|
||||
tier: u32,
|
||||
min_part_size: NVencAV1PartSize,
|
||||
max_part_size: NVencAV1PartSize,
|
||||
bitflags: u32,
|
||||
idr_period: u32,
|
||||
intra_refresh_period: u32,
|
||||
intra_refresh_count: u32,
|
||||
max_num_ref_frames_in_dpb: u32,
|
||||
num_tiles_columns: u32,
|
||||
num_tile_rows: u32,
|
||||
rsvd2: u32,
|
||||
tile_widths: *mut u32,
|
||||
tile_heights: *mut u32,
|
||||
max_temporal_layers_minus_1: u32,
|
||||
color_primaries: NVencVUIColorPrimaries,
|
||||
transfer_characteristics: NVencVUITransCharacteristics,
|
||||
matrix_coefficients: NVencVUIMatrixCoeffs,
|
||||
color_range: u32,
|
||||
chroma_sample_position: u32,
|
||||
use_b_frames_as_ref: NVencBFrameRefMode,
|
||||
film_grain_params: NVencFilmGrainParamsAV1,
|
||||
num_fwd_refs: NVencNumRefFrames,
|
||||
num_bwd_refs: NVencNumRefFrames,
|
||||
input_bit_depth: NVencBitDepth,
|
||||
ltr_num_frames: u32,
|
||||
num_temporal_layers: u32,
|
||||
tf_level: NVencTemporalFilterLevel,
|
||||
rsvd1: [u32; 230],
|
||||
rsvd3: [*mut c_void; 62],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencConfigH264MeOnly {
|
||||
bitflags: u32,
|
||||
rsvd1: [u32; 255],
|
||||
rsvd2: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencConfigHEVCMeOnly {
|
||||
rsvd: [u32; 256],
|
||||
rsvd1: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub union NVencCodecConfig {
|
||||
h264: ManuallyDrop<NVencConfigH264>,
|
||||
hevc: ManuallyDrop<NVencConfigHEVC>,
|
||||
av1: ManuallyDrop<NVencConfigAV1>,
|
||||
h264_meonly: ManuallyDrop<NVencConfigH264MeOnly>,
|
||||
hevc_meonly: ManuallyDrop<NVencConfigHEVCMeOnly>,
|
||||
rsvd: [u32; 320],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencConfig {
|
||||
pub version: u32,
|
||||
pub profile_guid: Guid,
|
||||
pub gop_len: u32,
|
||||
pub frame_interval_p: i32,
|
||||
mono_chrome_encoding: u32,
|
||||
frame_field_mode: NVencParamsFrameFieldMode,
|
||||
mv_precision: NVencMVPrecision,
|
||||
pub rc_params: NVencRcParams,
|
||||
encode_codec_config: NVencCodecConfig,
|
||||
rsvd: [u32; 278],
|
||||
rsvd2: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
pub const NV_ENC_CONFIG_VER: u32 = struct_version(9) | (1 << 31);
|
||||
|
||||
#[bitfields::bitfield(u32)]
|
||||
pub struct NVencInitializeParamsBitfields {
|
||||
#[bits(1)]
|
||||
pub report_slice_offsets: bool,
|
||||
#[bits(1)]
|
||||
pub enable_sub_frame_write: bool,
|
||||
#[bits(1)]
|
||||
pub enable_external_me_hints: bool,
|
||||
#[bits(1)]
|
||||
pub enable_me_only_mode: bool,
|
||||
#[bits(1)]
|
||||
pub enable_weighted_prediction: bool,
|
||||
#[bits(4)]
|
||||
pub split_encode_mode: u8,
|
||||
#[bits(1)]
|
||||
pub enable_output_in_vid_mem: bool,
|
||||
#[bits(1)]
|
||||
pub enable_recon_frame_output: bool,
|
||||
#[bits(1)]
|
||||
pub enable_output_stats: bool,
|
||||
#[bits(1)]
|
||||
pub enable_uni_directional_b: bool,
|
||||
#[bits(19)]
|
||||
pub rsvd: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencInitializeParams {
|
||||
pub version: u32,
|
||||
pub encode_guid: Guid,
|
||||
pub preset_guid: Guid,
|
||||
pub encode_width: u32,
|
||||
pub encode_height: u32,
|
||||
pub dar_width: u32,
|
||||
pub dar_height: u32,
|
||||
pub frame_rate_num: u32,
|
||||
pub frame_rate_den: u32,
|
||||
pub enable_encode_async: u32,
|
||||
pub enable_ptd: u32,
|
||||
pub bit_flags: NVencInitializeParamsBitfields,
|
||||
pub(crate) priv_data_size: u32,
|
||||
pub(crate) rsvd: u32,
|
||||
pub(crate) priv_data: *mut c_void,
|
||||
pub encode_config: *mut NVencConfig,
|
||||
pub max_encode_width: u32,
|
||||
pub max_encode_height: u32,
|
||||
pub max_me_hint_counts_per_block: [NVencExternalMeHintCountsPerBlockType; 2],
|
||||
pub tuning_info: NVencTuningInfo,
|
||||
pub buffer_format: NVencBufferFormat,
|
||||
pub num_state_buffers: u32,
|
||||
pub output_stats_level: NVencOutputStatsLevel,
|
||||
pub(crate) rsvd1: [u32; 284],
|
||||
pub(crate) rsvd2: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
pub const NV_ENC_INITIALIZE_PARAMS_VER: u32 = struct_version(7) | (1 << 31);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencReconfigureParams {
|
||||
version: u32,
|
||||
rsvd: u32,
|
||||
re_init_encode_params: NVencInitializeParams,
|
||||
bitflags: u32,
|
||||
rsvd2: u32,
|
||||
}
|
||||
|
||||
pub const NV_ENC_RECONFIGURE_PARAMS_VER: u32 = struct_version(2) | (1 << 31);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencPresetConfig {
|
||||
pub version: u32,
|
||||
rsvd: u32,
|
||||
pub preset_cfg: NVencConfig,
|
||||
rsvd1: [u32; 256],
|
||||
rsvd2: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
pub const NV_ENC_PRESET_CONFIG_VER: u32 = struct_version(5) | (1 << 31);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencPicParamsMVC {
|
||||
version: u32,
|
||||
view_id: u32,
|
||||
temporal_id: u32,
|
||||
priority_id: u32,
|
||||
rsvd1: [u32; 12],
|
||||
rsvd2: [*mut c_void; 8],
|
||||
}
|
||||
|
||||
pub const NV_ENC_PIC_PARAMS_MVC_VER: u32 = struct_version(1);
|
||||
|
||||
#[repr(C)]
|
||||
pub union NVencPicParamsH264EXT {
|
||||
pub mvc_pic_params: ManuallyDrop<NVencPicParamsMVC>,
|
||||
pub rsvd1: [u32; 32],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencSeiPayload {
|
||||
pub payload_size: u32,
|
||||
pub payload_type: u32,
|
||||
pub payload: *mut u8,
|
||||
}
|
||||
|
||||
pub type NVencH264SeiPayload = NVencSeiPayload;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencPicParamsH264 {
|
||||
pub display_poc_syntax: u32,
|
||||
pub rsvd3: u32,
|
||||
pub ref_pic_flag: u32,
|
||||
pub color_plane_id: u32,
|
||||
pub force_intra_refresh_with_frame_count: u32,
|
||||
pub bitflags: u32,
|
||||
pub slice_type_data: *mut u8,
|
||||
pub slice_type_array_count: u32,
|
||||
pub sei_payload_array_count: u32,
|
||||
pub sei_payload_array: *mut NVencSeiPayload,
|
||||
pub slice_mode: u32,
|
||||
pub slice_mode_data: u32,
|
||||
pub ltr_mark_framce_idx: u32,
|
||||
pub ltr_use_frame_bitmap: u32,
|
||||
pub ltr_usage_mode: u32,
|
||||
pub force_intra_slice_count: u32,
|
||||
pub force_intra_slice_idx: *mut u32,
|
||||
pub h264_ext_pic_params: NVencPicParamsH264EXT,
|
||||
pub time_code: NVencTimeCode,
|
||||
pub rsvd: [u32; 202],
|
||||
pub rsvd2: [*mut c_void; 61],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencPicParamsHEVC {
|
||||
pub display_poc_syntax: u32,
|
||||
pub ref_pic_flag: u32,
|
||||
pub temporal_id: u32,
|
||||
pub force_intra_refresh_with_frame_count: u32,
|
||||
pub bitflags: u32,
|
||||
pub rsvd1: u32,
|
||||
pub slice_type_data: *mut u8,
|
||||
pub slice_type_array_count: u32,
|
||||
pub slice_mode: u32,
|
||||
pub slice_mode_data: u32,
|
||||
pub ltr_mark_frame_idx: u32,
|
||||
pub ltr_usage_frame_bitmap: u32,
|
||||
pub ltr_usage_mode: u32,
|
||||
pub sei_payload_array_count: u32,
|
||||
pub rsvd: u32,
|
||||
pub sei_payload_array: *mut NVencSeiPayload,
|
||||
pub time_code: NVencTimeCode,
|
||||
pub num_temporal_layers: u32,
|
||||
pub view_id: u32,
|
||||
pub _3d_reference_display_info: *mut HEVC3DReferenceDisplayInfo,
|
||||
pub max_cll: *mut ContentLightLevel,
|
||||
pub mastering_display: *mut MasteringDisplayInfo,
|
||||
pub rsvd2: [u32; 234],
|
||||
pub rsvd3: [*mut c_void; 58],
|
||||
}
|
||||
|
||||
pub type NVencAV1OBUPayload = NVencSeiPayload;
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencPicParamsAV1 {
|
||||
display_poc_syntax: u32,
|
||||
ref_pic_flag: u32,
|
||||
temporal_id: u32,
|
||||
force_intra_refresh_with_frame_count: u32,
|
||||
bitflags: u32,
|
||||
num_tile_columns: u32,
|
||||
num_tile_rows: u32,
|
||||
rsvd: u32,
|
||||
tile_widths: *mut u32,
|
||||
tile_heights: *mut u32,
|
||||
obu_payload_array_count: u32,
|
||||
rsvd1: u32,
|
||||
obu_payload_array: *mut NVencAV1OBUPayload,
|
||||
file_grain_params: *mut NVencFilmGrainParamsAV1,
|
||||
ltr_mark_frame_idx: u32,
|
||||
ltr_use_frame_bitmap: u32,
|
||||
num_temporal_layers: u32,
|
||||
rsvd4: u32,
|
||||
max_cll: *mut ContentLightLevel,
|
||||
mastering_display: *mut MasteringDisplayInfo,
|
||||
rsvd2: [u32; 242],
|
||||
rsvd3: [*mut c_void; 59],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub union NVencCodecPicParams {
|
||||
pub h264_pic_params: ManuallyDrop<NVencPicParamsH264>,
|
||||
pub hevc_pic_params: ManuallyDrop<NVencPicParamsHEVC>,
|
||||
av1_pic_params: ManuallyDrop<NVencPicParamsAV1>,
|
||||
pub rsvd: [u32; 256],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencPicParams {
|
||||
pub version: u32,
|
||||
pub input_width: u32,
|
||||
pub input_height: u32,
|
||||
pub input_pitch: u32,
|
||||
pub encode_pic_flags: u32,
|
||||
pub frame_idx: u32,
|
||||
pub input_time_stamp: u64,
|
||||
input_duration: u64,
|
||||
pub input_buffer: *mut c_void,
|
||||
pub output_bitstream: *mut c_void,
|
||||
// TODO: Windows only, needed for async
|
||||
completion_event: *mut c_void,
|
||||
pub buffer_format: NVencBufferFormat,
|
||||
pub picture_struct: NVencPicStruct,
|
||||
pub picture_type: NVencPicType,
|
||||
pub codec_pic_params: NVencCodecPicParams,
|
||||
me_hint_counts_per_block: [NVencExternalMeHintCountsPerBlockType; 2],
|
||||
me_external_hints: *mut NVencExternalMeHint,
|
||||
rsvd2: [u32; 7],
|
||||
rsvd5: [*mut c_void; 2],
|
||||
qp_delta_map: *mut i8,
|
||||
qp_deltra_map_size: u32,
|
||||
rsvd_bit_fields: u32,
|
||||
me_hint_ref_pic_dist: [u16; 2],
|
||||
rsvd4: u32,
|
||||
alpha_buffer: *mut c_void,
|
||||
me_external_sb_hints: *mut NVencExternalMeSbHint,
|
||||
me_sb_hints_count: u32,
|
||||
state_buffer_idx: u32,
|
||||
output_recon_buffer: *mut c_void,
|
||||
rsvd3: [u32; 284],
|
||||
rsvd6: [*mut c_void; 57],
|
||||
}
|
||||
|
||||
pub const NV_ENC_PIC_PARAMS_VER: u32 = struct_version(7) | (1 << 31);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencMeOnlyParams {
|
||||
version: u32,
|
||||
input_width: u32,
|
||||
input_height: u32,
|
||||
rsvd: u32,
|
||||
input_buffer: *mut c_void,
|
||||
reference_frame: *mut c_void,
|
||||
mv_buffer: *mut c_void,
|
||||
rsvd2: u32,
|
||||
buffer_formtat: NVencBufferFormat,
|
||||
view_id: u32,
|
||||
me_hint_counts_per_block: [NVencExternalMeHintCountsPerBlockType; 2],
|
||||
me_external_hints: NVencExternalMeHint,
|
||||
rsvd1: [u32; 241],
|
||||
rsvd3: [*mut c_void; 59],
|
||||
}
|
||||
|
||||
pub const NV_ENC_MEONLY_PARAMS_VER: u32 = struct_version(4);
|
||||
|
||||
#[bitfields::bitfield(u32)]
|
||||
pub struct NVencLockBitStreamBitFields {
|
||||
#[bits(1)]
|
||||
pub do_not_wait: bool,
|
||||
#[bits(1)]
|
||||
pub ltr_frame: bool,
|
||||
#[bits(1)]
|
||||
pub get_rc_stats: bool,
|
||||
#[bits(29)]
|
||||
rsvd: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencLockBitStream {
|
||||
pub version: u32,
|
||||
pub bit_fields: NVencLockBitStreamBitFields,
|
||||
pub output_bit_stream: *mut c_void,
|
||||
slice_offsets: *mut u32,
|
||||
frame_idx: u32,
|
||||
hw_encode_status: u32,
|
||||
num_slices: u32,
|
||||
pub bitstream_size_in_bytes: u32,
|
||||
output_time_stamp: u64,
|
||||
pub output_duration: u64,
|
||||
pub bitstream_buffer: *mut c_void,
|
||||
picture_type: NVencPicType,
|
||||
picture_struct: NVencPicStruct,
|
||||
framge_avg_qp: u32,
|
||||
frame_satd: u32,
|
||||
ltr_frame_idx: u32,
|
||||
ltr_frame_bitmap: u32,
|
||||
temporal_id: u32,
|
||||
inter_mb_count: u32,
|
||||
average_mvx: i32,
|
||||
average_mvy: i32,
|
||||
alpha_layer_size_in_bytes: u32,
|
||||
output_stats_ptr_size: u32,
|
||||
rsvd: u32,
|
||||
output_stats_ptr: *mut c_void,
|
||||
frame_idx_display: u32,
|
||||
rsvd1: [u32; 219],
|
||||
rsvd2: [*mut c_void; 63],
|
||||
rsvd_interanl: [u32; 8],
|
||||
}
|
||||
|
||||
pub const NV_ENC_LOCK_BITSTREAM_VER: u32 = struct_version(2) | (1 << 31);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencLockInputBuffer {
|
||||
pub version: u32,
|
||||
pub bitflags: u32,
|
||||
pub input_buffer: *mut c_void,
|
||||
pub buffer_data_ptr: *mut c_void,
|
||||
pub pitch: u32,
|
||||
pub rsvd1: [u32; 251],
|
||||
pub rsvd2: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
pub const NV_ENC_LOCK_INPUT_BUFFER_VER: u32 = struct_version(1);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencMapInputResource {
|
||||
pub version: u32,
|
||||
sub_resource_idx: u32,
|
||||
input_resource: *mut c_void,
|
||||
pub registered_resource: *mut c_void,
|
||||
pub mapped_resource: *mut c_void,
|
||||
pub mapped_buffer_format: NVencBufferFormat,
|
||||
rsvd1: [u32; 251],
|
||||
rsvd: [*mut c_void; 63],
|
||||
}
|
||||
|
||||
pub const NV_ENC_MAP_INPUT_RESOURCE_VER: u32 = struct_version(4);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencInputResourceOpenGLTex {
|
||||
texture: u32,
|
||||
target: u32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencFencePointD3D12 {
|
||||
version: u32,
|
||||
rsvd: u32,
|
||||
p_fence: *mut c_void,
|
||||
wait_value: u64,
|
||||
signal_value: u64,
|
||||
bit_flags: u32,
|
||||
rsvd1: [u32; 7],
|
||||
}
|
||||
|
||||
pub const NV_ENC_FENCE_POINT_D3D12_VER: u32 = struct_version(1);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencInputResourceD3D12 {
|
||||
version: u32,
|
||||
rsvd: u32,
|
||||
input_buffer: *mut c_void,
|
||||
input_fence_point: NVencFencePointD3D12,
|
||||
rsvd1: [u32; 16],
|
||||
rsvd2: [*mut c_void; 16],
|
||||
}
|
||||
|
||||
pub const NV_ENC_INPUT_RESOURCE_D3D12_VER: u32 = struct_version(1);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencOutputResourceD3D12 {
|
||||
version: u32,
|
||||
rsvd: u32,
|
||||
output_buffer: *mut c_void,
|
||||
output_fence_point: NVencFencePointD3D12,
|
||||
rsvd1: [u32; 16],
|
||||
rsvd2: [*mut c_void; 16],
|
||||
}
|
||||
|
||||
pub const NV_ENC_OUTPUT_RESOURCE_D3D12_VER: u32 = struct_version(1);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencRegisterResource {
|
||||
pub version: u32,
|
||||
pub resource_type: NVencInputResourceType,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub pitch: u32,
|
||||
pub sub_resource_index: u32,
|
||||
pub resource_to_register: *mut c_void,
|
||||
pub registered_resource: *mut c_void,
|
||||
pub buffer_format: NVencBufferFormat,
|
||||
pub buffer_usage: NVencBufferUsage,
|
||||
pub input_fence_point: *mut NVencFencePointD3D12,
|
||||
pub chroma_offset: [u32; 2],
|
||||
pub chroma_offset_in: [u32; 2],
|
||||
pub(crate) rsvd1: [u32; 244],
|
||||
pub(crate) rsvd2: [*mut c_void; 61],
|
||||
}
|
||||
|
||||
pub const NV_ENC_REGISTER_RESOURCE_VER: u32 = struct_version(5);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencStat {
|
||||
version: u32,
|
||||
rsvd: u32,
|
||||
output_bit_stream: *mut c_void,
|
||||
bit_stream_size: u32,
|
||||
pic_type: u32,
|
||||
last_valid_byte_offset: u32,
|
||||
slice_offsets: [u32; 16],
|
||||
pic_idx: u32,
|
||||
frame_avg_qp: u32,
|
||||
bit_flags: u32,
|
||||
ltr_frame_idx: u32,
|
||||
intra_mb_count: u32,
|
||||
inter_mb_count: u32,
|
||||
average_mv_x: i32,
|
||||
average_mv_y: i32,
|
||||
rsvd1: [u32; 227],
|
||||
rsvd2: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
pub const NV_ENC_STAT_VER: u32 = struct_version(2);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencSequenceParamPayload {
|
||||
version: u32,
|
||||
in_buffer_size: u32,
|
||||
sps_id: u32,
|
||||
pps_id: u32,
|
||||
spspps_buffer: *mut c_void,
|
||||
output_spspps_payload_size: *mut u32,
|
||||
rsvd: [u32; 250],
|
||||
rsvd2: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
pub const NV_ENC_SEQUENCE_PARAM_PAYLOAD_VER: u32 = struct_version(1);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencEventParams {
|
||||
version: u32,
|
||||
rsvd: u32,
|
||||
completion_event: *mut c_void,
|
||||
rsvd1: [u32; 254],
|
||||
rsvd2: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
pub const NV_ENC_EVENT_PARAMS_VER: u32 = struct_version(2);
|
||||
|
||||
#[repr(C)]
|
||||
pub struct NVencOpenEncodeSessionExParams {
|
||||
pub version: u32,
|
||||
pub device_type: NVencDeviceType,
|
||||
pub device: *mut c_void,
|
||||
rsvd: *mut c_void,
|
||||
pub api_version: u32,
|
||||
rsvd1: [u32; 253],
|
||||
rsvd2: [*mut c_void; 64],
|
||||
}
|
||||
|
||||
pub const NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER: u32 = struct_version(1);
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
//! Contains the nvenc version numbers, encoded as they are in C, for the version this crate was built against
|
||||
|
||||
pub const NVENC_MAJOR_VERSION: u16 = 13;
|
||||
pub const NVENC_MINOR_VERSION: u8 = 0;
|
||||
|
||||
pub const NVENC_API_VERSION: u32 = NVENC_MAJOR_VERSION as u32 | (NVENC_MINOR_VERSION as u32) << 24;
|
||||
|
||||
pub const NVENC_API_LIST_VERSION: u32 = struct_version(2);
|
||||
|
||||
pub const fn struct_version(nvenc_api_version: u8) -> u32 {
|
||||
NVENC_API_VERSION | (nvenc_api_version as u32) << 16 | 0x7 << 28
|
||||
}
|
||||
+69
-4
@@ -110,6 +110,8 @@ struct Stats {
|
||||
agent_sent: AtomicU64,
|
||||
/// H.264 解码失败计数(关键帧自愈验证指标)
|
||||
dec_fail: AtomicU64,
|
||||
/// 当前是否处于「参考链断裂、等待 IDR」状态(HUD 显示 RECOVER)
|
||||
awaiting_idr: AtomicBool,
|
||||
/// 帧到达间隔环形缓冲
|
||||
gaps: std::sync::Mutex<GapRing>,
|
||||
}
|
||||
@@ -129,6 +131,7 @@ impl Default for Stats {
|
||||
agent_lost: AtomicU64::new(0),
|
||||
agent_sent: AtomicU64::new(0),
|
||||
dec_fail: AtomicU64::new(0),
|
||||
awaiting_idr: AtomicBool::new(true),
|
||||
gaps: std::sync::Mutex::new(GapRing {
|
||||
last: None,
|
||||
ring: [0; 64],
|
||||
@@ -218,9 +221,27 @@ async fn run(addr: String, port: u16, code: String) -> Result<()> {
|
||||
|
||||
let (mut send, mut recv) = conn.open_bi().await?;
|
||||
|
||||
write_control(&mut send, &ControlMsg::Auth { code }).await?;
|
||||
write_control(
|
||||
&mut send,
|
||||
&ControlMsg::Auth {
|
||||
code,
|
||||
proto_version: common::PROTO_VERSION,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
match read_control(&mut recv).await? {
|
||||
ControlMsg::AuthOk { width, height } => {
|
||||
ControlMsg::AuthOk {
|
||||
width,
|
||||
height,
|
||||
proto_version,
|
||||
} => {
|
||||
// 版本已在 Auth 里声明,这里再核对一次:防止被控端是旧版却"接受"了认证
|
||||
if proto_version != common::PROTO_VERSION {
|
||||
bail!(
|
||||
"protocol version mismatch: server={proto_version}, client={}",
|
||||
common::PROTO_VERSION
|
||||
);
|
||||
}
|
||||
println!("[viewer] auth ok, remote desktop {width}x{height}");
|
||||
// 流 2:键鼠输入 + Ping/Pong 独立通道,不与视频流竞争
|
||||
let (isend, irecv) = conn.open_bi().await?;
|
||||
@@ -257,6 +278,15 @@ fn session(
|
||||
let mut last_kfreq = Instant::now();
|
||||
let mut skip_stale = 0u64;
|
||||
let mut last_arr: Option<Instant> = None;
|
||||
// 「等待 IDR」状态:参考链一旦断裂(跳帧或解码失败),后续 P 帧**必然**
|
||||
// 解不出来。此时必须停止喂解码器,只等 IDR 重新同步。
|
||||
//
|
||||
// 之前没有这个状态,后果是:跳掉一帧后,每个 P 帧都被送进 decoder →
|
||||
// 每个都失败 → 每个都打日志 → 每个都触发一次 KeyFrameReq。
|
||||
// 表现为「decode failed」刷屏,且解码器反复在错误状态里挣扎。
|
||||
//
|
||||
// 放在 Stats 里(而非局部变量)以便 HUD 显示 RECOVER 状态。
|
||||
// 初始 true:入会时已发 KeyFrameReq,拿到第一个 IDR 前不该硬解。
|
||||
tokio::spawn(async move {
|
||||
let mut size_logged = false;
|
||||
loop {
|
||||
@@ -304,7 +334,10 @@ fn session(
|
||||
"[viewer] backlog: stale burst frame skipped (total {skip_stale})"
|
||||
);
|
||||
}
|
||||
// 限频请求关键帧:积压排空后从 IDR 重新衔接画面
|
||||
// 跳帧直接切断 H.264 参考链:之后所有 P 帧都解不出来。
|
||||
// 立刻转入「等待 IDR」并请求关键帧,绝不能让后续 P 帧
|
||||
// 继续喂给解码器(那是必然失败的徒劳尝试)。
|
||||
stats.awaiting_idr.store(true, Ordering::Relaxed);
|
||||
if last_kfreq.elapsed() >= Duration::from_millis(200) {
|
||||
last_kfreq = Instant::now();
|
||||
if let Ok(mut slot) = latest.lock() {
|
||||
@@ -313,6 +346,22 @@ fn session(
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 参考链未恢复时,P 帧一律跳过——此为正确性要求,不是优化。
|
||||
if stats.awaiting_idr.load(Ordering::Relaxed) && !hdr.keyframe {
|
||||
skip_stale += 1;
|
||||
// 补发请求,防止首次 KeyFrameReq 丢失(时间闸限频)
|
||||
if last_kfreq.elapsed() >= Duration::from_millis(200) {
|
||||
last_kfreq = Instant::now();
|
||||
if let Ok(mut slot) = latest.lock() {
|
||||
*slot = Some((0, 0, Vec::new()));
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// 拿到 IDR 才能重新开始解码
|
||||
if hdr.keyframe {
|
||||
stats.awaiting_idr.store(false, Ordering::Relaxed);
|
||||
}
|
||||
match decoder.decode(&payload) {
|
||||
Ok(Some(yuv)) => {
|
||||
let (dw, dh) = yuv.dimensions();
|
||||
@@ -334,7 +383,8 @@ fn session(
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
// 解码异常:请求被控端强制关键帧
|
||||
// 解码异常:参考链已不可信,转入等待 IDR
|
||||
stats.awaiting_idr.store(true, Ordering::Relaxed);
|
||||
stats.dec_fail.fetch_add(1, Ordering::Relaxed);
|
||||
eprintln!("[viewer] decode failed: {e}, request keyframe");
|
||||
if let Ok(mut slot) = latest.lock() {
|
||||
@@ -577,6 +627,21 @@ fn session(
|
||||
("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 }),
|
||||
(
|
||||
// 参考链断裂、正在等 IDR 的显式指示:此时画面冻结是**预期行为**,
|
||||
// 不是故障。没有这个指示,用户会以为软件卡死。
|
||||
"STATE",
|
||||
&if stats.awaiting_idr.load(Ordering::Relaxed) {
|
||||
"RECOVER".to_string()
|
||||
} else {
|
||||
"OK".to_string()
|
||||
},
|
||||
if stats.awaiting_idr.load(Ordering::Relaxed) {
|
||||
C_AMBER
|
||||
} else {
|
||||
C_GREEN
|
||||
},
|
||||
),
|
||||
("RATE", &format!("{:.0}MBP", hud_mbps), C_GRAY),
|
||||
("RES", &format!("{w}x{h}"), C_GRAY),
|
||||
],
|
||||
|
||||
+53
-8
@@ -189,7 +189,9 @@ pub enum Path { Direct { addr: SocketAddr }, Relay { endpoint: EndpointId } }
|
||||
|
||||
---
|
||||
|
||||
## 6. ⚠️ 必须现在拍板:手机主控 vs 不做中继
|
||||
## 6. ✅ 已裁决:不做中继(2026-10-28)
|
||||
|
||||
> **裁决结论**:公司不提供中继服务器,中继不做。以下保留原始分析以免后人重蹈讨论。
|
||||
|
||||
**手机在蜂窝网络下几乎 100% 处于运营商级 NAT(CGNAT),打洞打不通。**
|
||||
这不是实现问题,是网络事实——`100.64.0.0/10` 那段地址根本没给你可映射的公网端口。
|
||||
@@ -197,16 +199,56 @@ pub enum Path { Direct { addr: SocketAddr }, Relay { endpoint: EndpointId } }
|
||||
后果:**"主控端支持安卓/iOS" + "不做中继" = 手机只能在 WiFi 下用。**
|
||||
地铁上、外面、用流量,一律连不上。
|
||||
|
||||
三条出路,必须选一条:
|
||||
三条出路:
|
||||
|
||||
| 方案 | 代价 | 说明 |
|
||||
|---|---|---|
|
||||
| **A. 只靠 IPv6** | 部分用户不可用 | 国内三大运营商 v6 覆盖率高,手机 v6 可用性远好于 v4 公网。**推荐先做,能覆盖大多数** |
|
||||
| **B. 补一个只为移动端服务的中继** | 视频带宽成本 | 可用率 100%,但账单跟着视频流量走 |
|
||||
| **C. 接受"手机只能 WiFi"** | 产品力受限 | 零成本,但要写进产品说明,别让用户以为坏了 |
|
||||
| **A. 只靠 IPv6** | 部分用户不可用 | 国内三大运营商 v6 覆盖率高,手机 v6 可用性远好于 v4 公网 |
|
||||
| B. 补一个只为移动端服务的中继 | 视频带宽成本 | **已排除:公司不提供额外服务器** |
|
||||
| C. 接受"手机只能 WiFi" | 产品力受限 | 零成本,但要写进产品说明 |
|
||||
|
||||
**我的建议:A 先做(架构里 v6 是一等公民),B 的接口留在 `PathProvider` 里不给实现,等真要推移动端再决定。**
|
||||
不要现在就把路堵死——留接口的成本几乎为零,推倒重来成本极高。
|
||||
### 6.1 裁决内容与边界(2026-10-28)
|
||||
|
||||
| 项 | 裁决 |
|
||||
|---|---|
|
||||
| 中继 / TURN / relay 服务器 | **不做**。公司不提供额外服务器,且无预算计划 |
|
||||
| IPv6 | **一等公民**。候选地址排序 v6 优先,`signaling.rs` 已实现 |
|
||||
| 中继接口预留 | **不预留**。不写 `PathProvider` trait、不写 `Path::Relay` 变体 |
|
||||
| 连接层边界 | **仍要做**。理由见 §6.2,与中继无关 |
|
||||
|
||||
**为什么不留中继接口**:留接口的收益是「将来补中继不用动会话层」,但既然中继已被排除,
|
||||
`Path::Relay` 这类变体永远不会被构造,只会产生 `dead_code` 警告,要么 `#[allow]` 要么日后删除。
|
||||
**零收益的预留就是纯负担。**
|
||||
|
||||
### 6.2 连接层边界与中继接口是两件事
|
||||
|
||||
这两件事常被混为一谈,必须分开:
|
||||
|
||||
| | 内容 | 现在做不做 |
|
||||
|---|---|---|
|
||||
| **连接层边界** | 把"怎么连"(信令/打洞/直连)与"连上干什么"(收流/解码/渲染)分开 | ✅ **要做** |
|
||||
| **中继接口** | `enum Path { Direct, Relay }` 这类类型预留 | ❌ **不做** |
|
||||
|
||||
连接层边界本身就有独立价值,与中继无关:
|
||||
|
||||
1. **连接方式确定会变**:局域网 `IP:端口` → 将来设备码 + 打洞,这是已定的路线;
|
||||
2. **安卓主控要复用**:`viewer-core` 收流解码渲染不应知道对面是怎么连上的;
|
||||
3. **可测性**:解码逻辑能脱离真连接单独测试。
|
||||
|
||||
做法是把它抽成一个函数边界(输入目标描述、返回 `quinn::Connection`),
|
||||
**不是**定义 trait 或枚举。
|
||||
|
||||
### 6.3 对产品的影响(必须写进对外说明)
|
||||
|
||||
| 网络环境 | 能否连接 |
|
||||
|---|---|
|
||||
| 双方任一侧无公网 IPv6,且处于 CGNAT 后 | ❌ **无法连接,无解** |
|
||||
| 双方都有公网 IPv6 | ✅ 可直连 |
|
||||
| 局域网 / 双方都有可打洞的公网 IPv4 | ✅ 可直连 |
|
||||
| 对称 NAT(双方) | ❌ 通常不通 |
|
||||
|
||||
**这不是 bug,是已知边界。** 用户在外网连不上时,UI 必须给出可执行提示
|
||||
(`PunchFailReason::user_hint()` 已提供文案),不能停留在"连接中"。
|
||||
|
||||
---
|
||||
|
||||
@@ -263,7 +305,10 @@ pub enum Path { Direct { addr: SocketAddr }, Relay { endpoint: EndpointId } }
|
||||
|
||||
## 10. 开放问题(需要拍板)
|
||||
|
||||
1. 中继:确认走 §6 的 A 方案(v6 优先、留接口不实现)?
|
||||
> §10 第 1 条已于 2026-10-28 裁决,见 §6。
|
||||
|
||||
1. ~~中继:确认走 §6 的 A 方案(v6 优先、留接口不实现)?~~
|
||||
**已裁决:不做中继、不留接口、v6 优先、但保留连接层边界。详见 §6.1 / §6.2。**
|
||||
2. 移动端:先 Android 还是直接砍 iOS?
|
||||
3. UI 框架:确认 Tauri v2,还是先原生桌面 + 后面再说移动?
|
||||
4. 账号体系:现在是"输验证码",要支持设备列表就得有账号/设备 ID 体系——什么时候做?
|
||||
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
# 打洞与信令设计(无中继方案)
|
||||
|
||||
> 状态:协议契约已落在 `common/src/signaling.rs`,待 A 过审后合入 main。
|
||||
> 状态:协议契约已落在 `common/src/signaling.rs`,已合入 main(`703eb64`)。
|
||||
> 决策:**不做中继(无 TURN / coturn)**,服务器只保留信令 + STUN 职能。
|
||||
> **裁决确认(2026-10-28)**:公司不提供中继服务器,本决策为最终结论,
|
||||
> 同时**不预留中继接口**(理由见《多平台架构设计.md》§6.1 / §6.2)。
|
||||
> 关联:`构建运行手册.md` §7 第 4 项、`Git协作规范.md` 二·B 第 ③ 项。
|
||||
|
||||
## 1. 结论先行:信令不是中继
|
||||
|
||||
@@ -20,31 +20,34 @@
|
||||
- `rustup default stable-x86_64-pc-windows-gnu`,装到 `%USERPROFILE%\.cargo\bin`
|
||||
- MinGW-w64:`D:\mingw64\bin`(gcc 16.2.0)
|
||||
- 构建时 PATH 需同时包含上面两个目录(缺 MinGW 会在链接 openh264 的 C++ 时失败)
|
||||
- **cargo 源**:国内直连 crates.io 会超时(cargo 不读系统代理),需配置镜像。已在本机
|
||||
`%USERPROFILE%\.cargo\config.toml` 写入 USTC 源:
|
||||
- **cargo 网络**:国内直连 crates.io 会超时,且 **cargo 不读 Windows 系统代理**,必须显式告诉它。
|
||||
已在本机 `%USERPROFILE%\.cargo\config.toml` 写入(Clash 之类混合端口,按实际改):
|
||||
|
||||
```toml
|
||||
[source.crates-io]
|
||||
replace-with = "ustc"
|
||||
[source.ustc]
|
||||
registry = "sparse+https://mirrors.ustc.edu.cn/crates.io-index/"
|
||||
[http]
|
||||
proxy = "http://127.0.0.1:7897"
|
||||
```
|
||||
|
||||
⚠️ **不要用国内镜像做源替换**:实测 USTC 镜像的索引 `cksum` 与其实际下发的 `.crate`
|
||||
字节**不一致**(`nvenc 0.1.0`:索引写 `3227c3b0…`,包体实际 `ab6fde0e…`),
|
||||
会报 `failed to verify the checksum of nvenc`;且源替换会改变 registry 身份,
|
||||
使 `Cargo.lock` 里已有的 checksum 与全部缓存失配。**走官方源 + 代理最稳。**
|
||||
|
||||
> 注:旧版本手册写的 `D:\wordkb\...\uurev\tools\rust` 路径已不存在,按上面重新准备工具链即可。
|
||||
|
||||
## 3. 构建
|
||||
|
||||
```bash
|
||||
export RUSTUP_HOME="D:\\wordkb\\2026-10-22-15-52-47\\uurev\\tools\\rust\\rustup"
|
||||
export CARGO_HOME="D:\\wordkb\\2026-10-22-15-52-47\\uurev\\tools\\rust\\cargo"
|
||||
export PATH="/d/wordkb/2026-10-22-15-52-47/uurev/tools/rust/cargo/bin:/d/wordkb/2026-10-22-15-52-47/uurev/tools/mingw64/bin:$PATH"
|
||||
cd /d/wordkb/2026-10-22-15-52-47/mvp-remote
|
||||
# Git Bash 写法:PATH 需含 rustup 的 cargo 与 MinGW-w64
|
||||
export PATH="$USERPROFILE/.cargo/bin:/d/mingw64/bin:$PATH"
|
||||
cd /d/code/remote # 换成你自己的工作目录 / worktree
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
产物:
|
||||
- 被控端 `target/release/agent.exe`
|
||||
- 主控端 `target/release/viewer.exe`
|
||||
- 编码基准(可选)`target/release/encbench.exe`
|
||||
|
||||
## 4. 运行(两台机器,或同机双开验证)
|
||||
|
||||
@@ -164,9 +167,15 @@ viewer.exe --addr <被控机IP> --port 47800 --code 123456
|
||||
bash scripts/linux-smoke.sh
|
||||
```
|
||||
|
||||
脚本自建 Xvfb 虚拟屏后跑端到端测试,覆盖:X11 采集 → openh264 编码 → QUIC 下行解帧、
|
||||
验证码认证(含**错误验证码被拒**)、Ping/Pong(含下行丢包统计字段)、静止桌面下的关键帧
|
||||
恢复,以及**输入注入实证**——注入鼠标移动后,从另一条 X11 连接读回指针真实坐标。
|
||||
脚本自建 Xvfb 虚拟屏后跑**单测 + 端到端**两层:
|
||||
|
||||
- 单测(不需要 X server):键码映射、坐标换算等纯逻辑;
|
||||
- 端到端:X11 采集 → openh264 编码 → QUIC 下行解帧、验证码认证(含**错误验证码被拒**)、
|
||||
Ping/Pong(含下行丢包统计字段)、静止桌面下的关键帧恢复,以及**输入注入实证**——
|
||||
注入鼠标移动后,从另一条 X11 连接读回指针真实坐标。
|
||||
|
||||
> 单测那层别省:坐标换算的取整边界就是靠它才发现的(集成测试有 ±2 像素容差,
|
||||
> 期望值写错也照样绿)。
|
||||
|
||||
> 静止关键帧那条用例做过**变异测试**:把修复退回会失败、恢复后通过,确认断言不是摆设。
|
||||
> 注意扰动量必须小(画 64x64 小方块)——整屏刷白会触发编码器场景切换自动 IDR,
|
||||
|
||||
Reference in new issue
Block a user