feat(agent): NVENC 硬编接入(GPU 零拷贝 ARGB 直通,软编自动兜底)
- 编码器自动选择:NVENC(H264 High/CBR 5Mbps/UltraLowLatency/无B帧/按需IDR)→ openh264 兜底 - Duplication 增加 GPU 中间纹理(B8G8R8A8 DEFAULT),NVENC register_resource_dx11 直读,颜色转换完全省略 - KeyFrameReq → encode_picture(picture_type=IDR),静止 500ms 重编码 P 帧 - windows 0.58→0.62(nvenc crate 依赖 0.62,运行时 dlopen nvEncodeAPI64.dll 零 SDK 依赖) - 实测(GTX 1650 Ti 回环):encode 41ms→4ms/帧(10x),convert 0ms,viewer 解码正常
This commit is contained in:
parent
94c45aff3f
commit
ddbb53d41a
109
Cargo.lock
generated
109
Cargo.lock
generated
@ -9,6 +9,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"common",
|
"common",
|
||||||
"lz4_flex",
|
"lz4_flex",
|
||||||
|
"nvenc",
|
||||||
"openh264",
|
"openh264",
|
||||||
"quinn",
|
"quinn",
|
||||||
"rcgen",
|
"rcgen",
|
||||||
@ -44,6 +45,27 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitfields"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef6e59298da389bc0649c7463856b34c6e17fe542f88939426ede4436c6b1195"
|
||||||
|
dependencies = [
|
||||||
|
"bitfields-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitfields-impl"
|
||||||
|
version = "1.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f2c044f98f86f15414668d6c8187c7e4fadab1ad2b31680f648703e0fe07c555"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "1.3.2"
|
version = "1.3.2"
|
||||||
@ -567,6 +589,17 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nvenc"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3227c3b09b1e042dab7d8d82cea77c0404e7d0c2bbb92e7e87cb319749cd20dc"
|
||||||
|
dependencies = [
|
||||||
|
"bitfields",
|
||||||
|
"libloading",
|
||||||
|
"windows",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
@ -1512,32 +1545,54 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.58.0"
|
version = "0.62.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
|
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
|
||||||
|
dependencies = [
|
||||||
|
"windows-collections",
|
||||||
|
"windows-core",
|
||||||
|
"windows-future",
|
||||||
|
"windows-numerics",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-collections"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-core",
|
"windows-core",
|
||||||
"windows-targets",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.58.0"
|
version = "0.62.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement",
|
"windows-implement",
|
||||||
"windows-interface",
|
"windows-interface",
|
||||||
|
"windows-link",
|
||||||
"windows-result",
|
"windows-result",
|
||||||
"windows-strings",
|
"windows-strings",
|
||||||
"windows-targets",
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-future"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-core",
|
||||||
|
"windows-link",
|
||||||
|
"windows-threading",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-implement"
|
name = "windows-implement"
|
||||||
version = "0.58.0"
|
version = "0.60.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
|
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1546,9 +1601,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-interface"
|
name = "windows-interface"
|
||||||
version = "0.58.0"
|
version = "0.59.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
|
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -1562,22 +1617,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-numerics"
|
||||||
version = "0.2.0"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
|
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-targets",
|
"windows-core",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-strings"
|
name = "windows-strings"
|
||||||
version = "0.1.0"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-result",
|
"windows-link",
|
||||||
"windows-targets",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1614,6 +1678,15 @@ dependencies = [
|
|||||||
"windows_x86_64_msvc",
|
"windows_x86_64_msvc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-threading"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows_aarch64_gnullvm"
|
name = "windows_aarch64_gnullvm"
|
||||||
version = "0.52.6"
|
version = "0.52.6"
|
||||||
|
|||||||
@ -11,5 +11,5 @@ quinn = "0.11"
|
|||||||
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "logging"] }
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "logging"] }
|
||||||
rcgen = { version = "0.13", default-features = false, features = ["ring", "pem"] }
|
rcgen = { version = "0.13", default-features = false, features = ["ring", "pem"] }
|
||||||
lz4_flex = "0.11"
|
lz4_flex = "0.11"
|
||||||
windows = "0.58"
|
windows = "0.62"
|
||||||
minifb = "0.27"
|
minifb = "0.27"
|
||||||
|
|||||||
@ -12,6 +12,7 @@ rustls.workspace = true
|
|||||||
rcgen.workspace = true
|
rcgen.workspace = true
|
||||||
lz4_flex.workspace = true
|
lz4_flex.workspace = true
|
||||||
openh264 = { version = "0.9.9", features = ["source"] }
|
openh264 = { version = "0.9.9", features = ["source"] }
|
||||||
|
nvenc = "0.1.0"
|
||||||
windows = { workspace = true, features = [
|
windows = { workspace = true, features = [
|
||||||
"Win32_Foundation",
|
"Win32_Foundation",
|
||||||
"Win32_Graphics_Direct3D",
|
"Win32_Graphics_Direct3D",
|
||||||
|
|||||||
@ -1,9 +1,17 @@
|
|||||||
//! DXGI Desktop Duplication 采集(Windows 8+)。
|
//! DXGI Desktop Duplication 采集(Windows 8+)。
|
||||||
//! 帧处理:BGRA → openh264 H.264 编码(屏幕内容实时档,5Mbps)→ 线格式。
|
//! 编码器自动选择:NVENC 硬编(N 卡:GPU 内 BGRA→H.264 零转换零拷贝直通)
|
||||||
|
//! → openh264 软编兜底(无 N 卡/驱动异常时自动启用)。
|
||||||
//! 支持主控端 KeyFrameReq 强制关键帧(解码异常恢复)。
|
//! 支持主控端 KeyFrameReq 强制关键帧(解码异常恢复)。
|
||||||
|
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use common::{encode_video_frame, VideoFrameHeader};
|
use common::{encode_video_frame, VideoFrameHeader};
|
||||||
|
use nvenc::bitstream::BitStream;
|
||||||
|
use nvenc::encoder::RegisteredResource;
|
||||||
|
use nvenc::session::{InitParams, Session};
|
||||||
|
use nvenc::sys::enums::{
|
||||||
|
NVencBufferFormat, NVencPicStruct, NVencPicType, NVencParamsRcMode, NVencTuningInfo,
|
||||||
|
};
|
||||||
|
use nvenc::sys::guids::{NV_ENC_CODEC_H264_GUID, NV_ENC_PRESET_P3_GUID};
|
||||||
use openh264::encoder::{
|
use openh264::encoder::{
|
||||||
BitRate, Encoder, EncoderConfig, FrameRate, IntraFramePeriod, RateControlMode, UsageType,
|
BitRate, Encoder, EncoderConfig, FrameRate, IntraFramePeriod, RateControlMode, UsageType,
|
||||||
};
|
};
|
||||||
@ -19,8 +27,9 @@ use windows::Win32::Graphics::Direct3D::{
|
|||||||
};
|
};
|
||||||
use windows::Win32::Graphics::Direct3D11::{
|
use windows::Win32::Graphics::Direct3D11::{
|
||||||
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Resource, ID3D11Texture2D,
|
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext, ID3D11Resource, ID3D11Texture2D,
|
||||||
D3D11_CREATE_DEVICE_FLAG, D3D11_CPU_ACCESS_READ, D3D11_MAP, D3D11_MAP_READ,
|
D3D11_BIND_RENDER_TARGET, D3D11_BIND_SHADER_RESOURCE, D3D11_CREATE_DEVICE_FLAG,
|
||||||
D3D11_MAPPED_SUBRESOURCE, D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_STAGING,
|
D3D11_CPU_ACCESS_READ, D3D11_MAP_READ, D3D11_MAPPED_SUBRESOURCE,
|
||||||
|
D3D11_SDK_VERSION, D3D11_TEXTURE2D_DESC, D3D11_USAGE_DEFAULT, D3D11_USAGE_STAGING,
|
||||||
};
|
};
|
||||||
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC};
|
use windows::Win32::Graphics::Dxgi::Common::{DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC};
|
||||||
use windows::Win32::Graphics::Dxgi::{
|
use windows::Win32::Graphics::Dxgi::{
|
||||||
@ -31,9 +40,12 @@ use windows::Win32::Graphics::Dxgi::{
|
|||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
pub struct Duplication {
|
pub struct Duplication {
|
||||||
_device: ID3D11Device,
|
pub device: ID3D11Device,
|
||||||
context: ID3D11DeviceContext,
|
context: ID3D11DeviceContext,
|
||||||
staging_res: ID3D11Resource,
|
staging_res: ID3D11Resource,
|
||||||
|
/// GPU 内中间纹理(NVENC 注册目标;DEFAULT usage,BGRA 与采集一致)
|
||||||
|
gpu_res: ID3D11Resource,
|
||||||
|
gpu_tex: ID3D11Texture2D,
|
||||||
dup: IDXGIOutputDuplication,
|
dup: IDXGIOutputDuplication,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
@ -47,13 +59,13 @@ impl Duplication {
|
|||||||
D3D11CreateDevice(
|
D3D11CreateDevice(
|
||||||
None,
|
None,
|
||||||
D3D_DRIVER_TYPE_HARDWARE,
|
D3D_DRIVER_TYPE_HARDWARE,
|
||||||
HMODULE::default(),
|
HMODULE(std::ptr::null_mut()),
|
||||||
D3D11_CREATE_DEVICE_FLAG(0),
|
D3D11_CREATE_DEVICE_FLAG(0),
|
||||||
Some(&[D3D_FEATURE_LEVEL_11_0]),
|
Some(&[D3D_FEATURE_LEVEL_11_0]),
|
||||||
D3D11_SDK_VERSION,
|
D3D11_SDK_VERSION,
|
||||||
Some(&mut device),
|
Some(&raw mut device),
|
||||||
None,
|
None,
|
||||||
Some(&mut context),
|
Some(&raw mut context),
|
||||||
)
|
)
|
||||||
.map_err(|e| anyhow!("D3D11CreateDevice failed: {e}"))?;
|
.map_err(|e| anyhow!("D3D11CreateDevice failed: {e}"))?;
|
||||||
let device = device.ok_or_else(|| anyhow!("no d3d device"))?;
|
let device = device.ok_or_else(|| anyhow!("no d3d device"))?;
|
||||||
@ -73,6 +85,7 @@ impl Duplication {
|
|||||||
let (width, height) = (desc.ModeDesc.Width, desc.ModeDesc.Height);
|
let (width, height) = (desc.ModeDesc.Width, desc.ModeDesc.Height);
|
||||||
anyhow::ensure!(width > 0 && height > 0, "bad desktop size");
|
anyhow::ensure!(width > 0 && height > 0, "bad desktop size");
|
||||||
|
|
||||||
|
// staging:软编路径 Map 读内存
|
||||||
let sdesc = D3D11_TEXTURE2D_DESC {
|
let sdesc = D3D11_TEXTURE2D_DESC {
|
||||||
Width: width,
|
Width: width,
|
||||||
Height: height,
|
Height: height,
|
||||||
@ -95,10 +108,26 @@ impl Duplication {
|
|||||||
let staging = staging_opt.ok_or_else(|| anyhow!("no staging texture"))?;
|
let staging = staging_opt.ok_or_else(|| anyhow!("no staging texture"))?;
|
||||||
let staging_res: ID3D11Resource = staging.cast()?;
|
let staging_res: ID3D11Resource = staging.cast()?;
|
||||||
|
|
||||||
|
// GPU 中间纹理:NVENC 路径的注册目标(DEFAULT usage,GPU 可读)
|
||||||
|
let gdesc = D3D11_TEXTURE2D_DESC {
|
||||||
|
Usage: D3D11_USAGE_DEFAULT,
|
||||||
|
BindFlags: (D3D11_BIND_RENDER_TARGET.0 | D3D11_BIND_SHADER_RESOURCE.0) as u32,
|
||||||
|
CPUAccessFlags: 0,
|
||||||
|
..sdesc
|
||||||
|
};
|
||||||
|
let mut gpu_opt: Option<ID3D11Texture2D> = None;
|
||||||
|
device
|
||||||
|
.CreateTexture2D(&gdesc, None, Some(&mut gpu_opt))
|
||||||
|
.map_err(|e| anyhow!("CreateTexture2D(gpu) failed: {e}"))?;
|
||||||
|
let gpu_tex = gpu_opt.ok_or_else(|| anyhow!("no gpu texture"))?;
|
||||||
|
let gpu_res: ID3D11Resource = gpu_tex.cast()?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
_device: device,
|
device,
|
||||||
context,
|
context,
|
||||||
staging_res,
|
staging_res,
|
||||||
|
gpu_res,
|
||||||
|
gpu_tex,
|
||||||
dup,
|
dup,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@ -106,7 +135,8 @@ impl Duplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// true=拿到新帧并已复制到 staging;false=超时无更新
|
/// true=拿到新帧并已复制;false=超时无更新。
|
||||||
|
/// 同时复制到 staging(软编读内存)与 GPU 中间纹理(NVENC 注册输入)。
|
||||||
pub fn acquire(&mut self, timeout_ms: u32) -> Result<bool> {
|
pub fn acquire(&mut self, timeout_ms: u32) -> Result<bool> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut info = DXGI_OUTDUPL_FRAME_INFO::default();
|
let mut info = DXGI_OUTDUPL_FRAME_INFO::default();
|
||||||
@ -116,6 +146,7 @@ impl Duplication {
|
|||||||
let res = resource.ok_or_else(|| anyhow!("no desktop resource"))?;
|
let res = resource.ok_or_else(|| anyhow!("no desktop resource"))?;
|
||||||
let src: ID3D11Resource = res.cast()?;
|
let src: ID3D11Resource = res.cast()?;
|
||||||
self.context.CopyResource(&self.staging_res, &src);
|
self.context.CopyResource(&self.staging_res, &src);
|
||||||
|
self.context.CopyResource(&self.gpu_res, &src);
|
||||||
let _ = self.dup.ReleaseFrame();
|
let _ = self.dup.ReleaseFrame();
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
@ -125,27 +156,7 @@ impl Duplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 把 staging 纹理映射到内存并去掉行间距
|
/// 在 staging 映射窗口内执行回调(软编路径)。回调参数 = (像素数据, 行距)。
|
||||||
pub fn copy_frame(&mut self) -> Result<Vec<u8>> {
|
|
||||||
unsafe {
|
|
||||||
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
|
||||||
self.context
|
|
||||||
.Map(&self.staging_res, 0, D3D11_MAP_READ, 0, Some(&mut mapped))
|
|
||||||
.map_err(|e| anyhow!("Map staging failed: {e}"))?;
|
|
||||||
let (w, h) = (self.width as usize, self.height as usize);
|
|
||||||
let pitch = mapped.RowPitch as usize;
|
|
||||||
let src = std::slice::from_raw_parts(mapped.pData as *const u8, pitch * h);
|
|
||||||
let mut out = Vec::with_capacity(w * h * 4);
|
|
||||||
for row in 0..h {
|
|
||||||
out.extend_from_slice(&src[row * pitch..row * pitch + w * 4]);
|
|
||||||
}
|
|
||||||
self.context.Unmap(&self.staging_res, 0);
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 在映射窗口内执行回调(避免 8MB 中间拷贝)。回调参数 = (像素数据, 行距)。
|
|
||||||
/// 映射内存仅在回调返回前有效。
|
|
||||||
pub fn with_mapped<R>(&mut self, f: impl FnOnce(&[u8], usize) -> R) -> Result<R> {
|
pub fn with_mapped<R>(&mut self, f: impl FnOnce(&[u8], usize) -> R) -> Result<R> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
let mut mapped = D3D11_MAPPED_SUBRESOURCE::default();
|
||||||
@ -162,6 +173,99 @@ impl Duplication {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// NVENC 硬编封装:H.264 High、CBR 码率控制、UltraLowLatency 调优、无 B 帧。
|
||||||
|
/// 输入 = 注册的 GPU 纹理(BGRA 与 DXGI 采集字节序一致,零转换)。
|
||||||
|
struct NvencEncoder {
|
||||||
|
encoder: nvenc::encoder::Encoder,
|
||||||
|
registered: RegisteredResource,
|
||||||
|
bitstream: BitStream,
|
||||||
|
frame_count: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NvencEncoder {
|
||||||
|
fn new(dup: &mut Duplication, bitrate_bps: u32) -> Result<Self> {
|
||||||
|
// 打开会话(无 N 卡 / 驱动不含编码库时在此失败)
|
||||||
|
let session: Session<_> = Session::open_dx(&dup.device)
|
||||||
|
.map_err(|e| anyhow!("open_dx: {e:?}"))?;
|
||||||
|
let codecs = session
|
||||||
|
.get_encode_codecs()
|
||||||
|
.map_err(|e| anyhow!("codecs: {e:?}"))?;
|
||||||
|
anyhow::ensure!(codecs.contains(&NV_ENC_CODEC_H264_GUID), "NVENC no H264 codec");
|
||||||
|
let (session, mut config) = session
|
||||||
|
.get_encode_preset_config_ex(
|
||||||
|
NV_ENC_CODEC_H264_GUID,
|
||||||
|
NV_ENC_PRESET_P3_GUID,
|
||||||
|
NVencTuningInfo::UltraLowLatency,
|
||||||
|
)
|
||||||
|
.map_err(|e| anyhow!("preset_config: {e:?}"))?;
|
||||||
|
// CBR 桌面实时档:码率恒定,延迟可预测(对标 UU 3.2Mbps@60fps)
|
||||||
|
config.preset_cfg.rc_params.rate_control_mode = NVencParamsRcMode::CBR;
|
||||||
|
config.preset_cfg.rc_params.average_bit_rate = bitrate_bps;
|
||||||
|
config.preset_cfg.gop_len = 0xffff_ffff; // 无周期 IDR,仅按需(KeyFrameReq)
|
||||||
|
config.preset_cfg.frame_interval_p = 1; // 无 B 帧
|
||||||
|
|
||||||
|
let init = InitParams {
|
||||||
|
encode_guid: NV_ENC_CODEC_H264_GUID,
|
||||||
|
preset_guid: NV_ENC_PRESET_P3_GUID,
|
||||||
|
resolution: [dup.width, dup.height],
|
||||||
|
aspect_ratio: [dup.width, dup.height],
|
||||||
|
frame_rate: [60, 1],
|
||||||
|
tuning_info: NVencTuningInfo::UltraLowLatency,
|
||||||
|
buffer_format: NVencBufferFormat::ARGB,
|
||||||
|
encode_config: &mut config.preset_cfg,
|
||||||
|
enable_ptd: true,
|
||||||
|
max_encoder_resolution: [dup.width, dup.height],
|
||||||
|
};
|
||||||
|
let encoder = session
|
||||||
|
.init_encoder(init)
|
||||||
|
.map_err(|e| anyhow!("init_encoder: {e:?}"))?;
|
||||||
|
let registered = encoder
|
||||||
|
.register_resource_dx11(&dup.gpu_tex, NVencBufferFormat::ARGB, 0)
|
||||||
|
.map_err(|e| anyhow!("register_resource: {e:?}"))?;
|
||||||
|
let bitstream = encoder
|
||||||
|
.create_bitstream_buffer()
|
||||||
|
.map_err(|e| anyhow!("bitstream: {e:?}"))?;
|
||||||
|
Ok(Self {
|
||||||
|
encoder,
|
||||||
|
registered,
|
||||||
|
bitstream,
|
||||||
|
frame_count: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 编码当前 GPU 纹理内容,返回 Annex B H.264 码流。
|
||||||
|
fn encode(&mut self, force_idr: bool, ts_ms: u64) -> Result<Vec<u8>> {
|
||||||
|
let ty = if force_idr {
|
||||||
|
NVencPicType::IDR
|
||||||
|
} else {
|
||||||
|
NVencPicType::P
|
||||||
|
};
|
||||||
|
self.encoder.encode_picture(
|
||||||
|
&self.registered,
|
||||||
|
&self.bitstream,
|
||||||
|
self.frame_count,
|
||||||
|
ts_ms,
|
||||||
|
NVencBufferFormat::ARGB,
|
||||||
|
NVencPicStruct::Frame,
|
||||||
|
ty,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.map_err(|e| anyhow!("encode_picture: {e:?}"))?;
|
||||||
|
self.frame_count += 1;
|
||||||
|
// 同步读出码流(NVENC 单帧编码 1-3ms,阻塞等待可接受)
|
||||||
|
let lock = self
|
||||||
|
.bitstream
|
||||||
|
.try_lock(true)
|
||||||
|
.map_err(|e| anyhow!("bitstream lock: {e:?}"))?;
|
||||||
|
let out = lock.as_slice().to_vec();
|
||||||
|
drop(lock);
|
||||||
|
if out.is_empty() {
|
||||||
|
anyhow::bail!("nvenc empty bitstream");
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn desktop_size() -> Result<(u32, u32)> {
|
pub fn desktop_size() -> Result<(u32, u32)> {
|
||||||
let d = Duplication::new()?;
|
let d = Duplication::new()?;
|
||||||
@ -186,116 +290,163 @@ pub fn run(tx: Sender<Option<Vec<u8>>>, keyreq: Arc<AtomicBool>) {
|
|||||||
dup.width, dup.height
|
dup.width, dup.height
|
||||||
);
|
);
|
||||||
|
|
||||||
// H.264 编码器:屏幕内容实时档 + 码率控制 + 4 线程(单线程 1080p 仅 ~20fps,
|
// ---- 编码器自动选择:NVENC 硬编优先,失败落回 openh264 软编 ----
|
||||||
// 多线程把编码压进 33ms 预算实现稳定 30fps)
|
enum EncoderKind {
|
||||||
let mut encoder = match Encoder::with_api_config(
|
Nvenc(Box<NvencEncoder>),
|
||||||
OpenH264API::from_source(),
|
Soft(Box<openh264::encoder::Encoder>),
|
||||||
EncoderConfig::new()
|
}
|
||||||
.bitrate(BitRate::from_bps(5_000_000))
|
let mut encoder: EncoderKind = match NvencEncoder::new(&mut dup, 5_000_000) {
|
||||||
.max_frame_rate(FrameRate::from_hz(60.0))
|
Ok(nv) => {
|
||||||
.rate_control_mode(RateControlMode::Bitrate)
|
eprintln!("[capture] encoder = NVENC hardware (CBR 5Mbps@60fps, ARGB direct)");
|
||||||
.usage_type(UsageType::ScreenContentRealTime)
|
EncoderKind::Nvenc(Box::new(nv))
|
||||||
.intra_frame_period(IntraFramePeriod::from_num_frames(300))
|
}
|
||||||
.num_threads(4),
|
|
||||||
) {
|
|
||||||
Ok(e) => e,
|
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("[capture] encoder init failed: {e}");
|
eprintln!("[capture] NVENC unavailable ({e}); fallback to openh264 software");
|
||||||
return;
|
match Encoder::with_api_config(
|
||||||
|
OpenH264API::from_source(),
|
||||||
|
EncoderConfig::new()
|
||||||
|
.bitrate(BitRate::from_bps(5_000_000))
|
||||||
|
.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))
|
||||||
|
.num_threads(4),
|
||||||
|
) {
|
||||||
|
Ok(e) => {
|
||||||
|
eprintln!("[capture] encoder = openh264 software (5Mbps@60fps, 4T)");
|
||||||
|
EncoderKind::Soft(Box::new(e))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[capture] encoder init failed: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut last_frame: Option<Vec<u8>> = None;
|
let mut last_frame: Option<Vec<u8>> = None; // 软编静止重发
|
||||||
let mut last_sent = Instant::now() - Duration::from_secs(10);
|
let mut last_sent = Instant::now() - Duration::from_secs(10);
|
||||||
// 编码实测 ~5ms/帧(1080p 屏幕内容实时档),60fps 只需 16.7ms 预算,
|
|
||||||
// 够用 → 帧率上限提到 60(对标 UU 60fps;带宽由 RC 压在 5Mbps 内)
|
|
||||||
let min_frame = Duration::from_millis(16); // 60fps 上限
|
let min_frame = Duration::from_millis(16); // 60fps 上限
|
||||||
let mut scratch: Vec<u8> = Vec::new(); // 行距不连续时的复用缓冲
|
let mut scratch: Vec<u8> = Vec::new(); // 软编:行距不连续时的复用缓冲
|
||||||
// 编码耗时统计(每 120 帧输出一次均值,ms)
|
// 编码耗时统计(每 120 帧输出一次均值,ms)
|
||||||
let mut stat_frames = 0u64;
|
let mut stat_frames = 0u64;
|
||||||
let mut stat_convert_ms = 0u64;
|
let mut stat_convert_ms = 0u64;
|
||||||
let mut stat_encode_ms = 0u64;
|
let mut stat_encode_ms = 0u64;
|
||||||
|
|
||||||
|
macro_rules! emit {
|
||||||
|
($payload:expr) => {{
|
||||||
|
let header = VideoFrameHeader {
|
||||||
|
width: dup.width,
|
||||||
|
height: dup.height,
|
||||||
|
ts_ms: start.elapsed().as_millis() as u64,
|
||||||
|
};
|
||||||
|
let frame = encode_video_frame(&header, &$payload);
|
||||||
|
if tx.send(Some(frame.clone())).is_err() {
|
||||||
|
return; // 会话结束
|
||||||
|
}
|
||||||
|
last_frame = Some(frame);
|
||||||
|
last_sent = Instant::now();
|
||||||
|
stat_frames += 1;
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let iter_start = Instant::now();
|
let iter_start = Instant::now();
|
||||||
match dup.acquire(80) {
|
match dup.acquire(80) {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
// 主控端请求关键帧(解码异常恢复)
|
// 主控端请求关键帧(解码异常恢复)
|
||||||
let force_key = keyreq.swap(false, Ordering::Relaxed);
|
let force_key = keyreq.swap(false, Ordering::Relaxed);
|
||||||
if force_key {
|
match &mut encoder {
|
||||||
encoder.force_intra_frame();
|
EncoderKind::Nvenc(enc) => {
|
||||||
}
|
|
||||||
// 映射窗口内直接做 BGRA→YUV(行距连续时零拷贝,否则复用 scratch)
|
|
||||||
let conv_start = Instant::now();
|
|
||||||
let dims = (dup.width as usize, dup.height as usize);
|
|
||||||
let conv = dup.with_mapped(|src, pitch| {
|
|
||||||
if pitch == dims.0 * 4 {
|
|
||||||
YUVBuffer::from_bgra8_source(BgraSliceU8::new(src, dims))
|
|
||||||
} else {
|
|
||||||
scratch.resize(dims.0 * dims.1 * 4, 0);
|
|
||||||
for (row, dst) in scratch.chunks_exact_mut(dims.0 * 4).enumerate() {
|
|
||||||
dst.copy_from_slice(
|
|
||||||
&src[row * pitch..row * pitch + dims.0 * 4],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
YUVBuffer::from_bgra8_source(BgraSliceU8::new(&scratch, dims))
|
|
||||||
}
|
|
||||||
});
|
|
||||||
match conv {
|
|
||||||
Ok(yuv) => {
|
|
||||||
stat_convert_ms += conv_start.elapsed().as_millis() as u64;
|
|
||||||
let enc_start = Instant::now();
|
let enc_start = Instant::now();
|
||||||
let r = encoder.encode(&yuv);
|
let r = enc.encode(force_key, start.elapsed().as_millis() as u64);
|
||||||
stat_encode_ms += enc_start.elapsed().as_millis() as u64;
|
stat_encode_ms += enc_start.elapsed().as_millis() as u64;
|
||||||
match r {
|
match r {
|
||||||
Ok(ebs) => {
|
Ok(payload) => emit!(payload),
|
||||||
let payload = ebs.to_vec();
|
Err(e) => eprintln!("[capture] nvenc encode failed: {e}"),
|
||||||
let header = VideoFrameHeader {
|
}
|
||||||
width: dup.width,
|
}
|
||||||
height: dup.height,
|
EncoderKind::Soft(enc) => {
|
||||||
ts_ms: start.elapsed().as_millis() as u64,
|
if force_key {
|
||||||
};
|
enc.force_intra_frame();
|
||||||
let frame = encode_video_frame(&header, &payload);
|
}
|
||||||
if tx.send(Some(frame.clone())).is_err() {
|
// 映射窗口内直接做 BGRA→YUV(行距连续时零拷贝,否则复用 scratch)
|
||||||
return; // 会话结束
|
let conv_start = Instant::now();
|
||||||
}
|
let dims = (dup.width as usize, dup.height as usize);
|
||||||
last_frame = Some(frame);
|
let conv = dup.with_mapped(|src, pitch| {
|
||||||
last_sent = Instant::now();
|
if pitch == dims.0 * 4 {
|
||||||
stat_frames += 1;
|
YUVBuffer::from_bgra8_source(BgraSliceU8::new(src, dims))
|
||||||
if stat_frames % 120 == 0 {
|
} else {
|
||||||
eprintln!(
|
scratch.resize(dims.0 * dims.1 * 4, 0);
|
||||||
"[capture] avg convert {}ms, encode {}ms/frame (last 120)",
|
for (row, dst) in
|
||||||
stat_convert_ms / 120,
|
scratch.chunks_exact_mut(dims.0 * 4).enumerate()
|
||||||
stat_encode_ms / 120
|
{
|
||||||
|
dst.copy_from_slice(
|
||||||
|
&src[row * pitch..row * pitch + dims.0 * 4],
|
||||||
);
|
);
|
||||||
stat_convert_ms = 0;
|
}
|
||||||
stat_encode_ms = 0;
|
YUVBuffer::from_bgra8_source(BgraSliceU8::new(&scratch, dims))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
match conv {
|
||||||
|
Ok(yuv) => {
|
||||||
|
stat_convert_ms +=
|
||||||
|
conv_start.elapsed().as_millis() as u64;
|
||||||
|
let enc_start = Instant::now();
|
||||||
|
let r = enc.encode(&yuv);
|
||||||
|
stat_encode_ms += enc_start.elapsed().as_millis() as u64;
|
||||||
|
match r {
|
||||||
|
Ok(ebs) => emit!(ebs.to_vec()),
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[capture] encode failed: {e}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("[capture] encode failed: {e}");
|
eprintln!("[capture] frame convert failed: {e}");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
|
||||||
eprintln!("[capture] frame convert failed: {e}");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// 帧间隔节流到 ~30fps
|
if stat_frames > 0 && stat_frames % 120 == 0 {
|
||||||
|
eprintln!(
|
||||||
|
"[capture] avg convert {}ms, encode {}ms/frame (last 120)",
|
||||||
|
stat_convert_ms / 120,
|
||||||
|
stat_encode_ms / 120
|
||||||
|
);
|
||||||
|
stat_convert_ms = 0;
|
||||||
|
stat_encode_ms = 0;
|
||||||
|
}
|
||||||
|
// 帧间隔节流到 ~60fps
|
||||||
let el = iter_start.elapsed();
|
let el = iter_start.elapsed();
|
||||||
if el < min_frame {
|
if el < min_frame {
|
||||||
std::thread::sleep(min_frame - el);
|
std::thread::sleep(min_frame - el);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(false) => {
|
Ok(false) => {
|
||||||
// 桌面静止:低频重发最后一帧(重复 P 帧,解码端画面不变)
|
// 桌面静止:低频重发(丢包后恢复 + 会话保活)。
|
||||||
|
// NVENC 重编当前 GPU 帧(输出极小 P 帧);软编重发最后一帧副本。
|
||||||
if last_sent.elapsed() > Duration::from_millis(500) {
|
if last_sent.elapsed() > Duration::from_millis(500) {
|
||||||
if let Some(f) = last_frame.as_ref() {
|
match &mut encoder {
|
||||||
if tx.send(Some(f.clone())).is_err() {
|
EncoderKind::Nvenc(enc) => {
|
||||||
return;
|
// 重编当前 GPU 帧(内容未变,输出极小 P 帧)
|
||||||
|
let r = enc.encode(false, start.elapsed().as_millis() as u64);
|
||||||
|
match r {
|
||||||
|
Ok(payload) => emit!(payload),
|
||||||
|
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()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
last_sent = Instant::now();
|
|
||||||
}
|
}
|
||||||
|
last_sent = Instant::now();
|
||||||
} else {
|
} else {
|
||||||
std::thread::sleep(Duration::from_millis(8));
|
std::thread::sleep(Duration::from_millis(8));
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user