feat(host/windows): two-process secure-desktop step 4 — spawn helper + relay AUs
The SYSTEM host now sources the normal-desktop video from a user-session WGC helper instead of capturing in-process (WGC won't activate as SYSTEM). New `capture/wgc_relay.rs`: `HelperRelay::spawn` launches `m3-host wgc-helper` in the interactive user session via CreateProcessAsUserW (WTSQueryUserToken → DuplicateTokenEx(TokenPrimary) → lpDesktop="winsta0\\default", CREATE_NO_WINDOW) with three anonymous pipes — stdout (framed Annex-B AUs → parsed back to RelayAu), stdin (control: force-keyframe), stderr (helper logs → host tracing). The host holds the SudoVDA keepalive (sole isolation/topology owner); the helper captures by GDI name only. m3.rs: `virtual_stream` dispatches to the new `virtual_stream_relay` when `should_use_helper()` (running as SYSTEM, or PUNKTFUNK_FORCE_HELPER; disable with PUNKTFUNK_NO_HELPER). The relay loop feeds the existing send thread — same FEC/seal/paced-send path. Reconfigure rebuilds the output + re-spawns the helper; keyframe requests forward over the control pipe; helper pts_ns (same-machine monotonic clock) is used directly as capture_ns. Disconnect ends the stream (step 6 adds the relaunch watchdog). wgc_helper.rs: reads the stdin control byte to request an IDR; --bit-depth flag threaded through so SDR 10-bit (Main10) negotiation reaches the helper's encoder. cfg-gated windows-only; Linux/macOS build unaffected. Step 5 (DesktopWatcher mux to host DDA on the Winlogon secure desktop) is next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,9 @@
|
||||
use crate::capture::{dxgi::WinCaptureTarget, wgc::WgcCapturer, Capturer};
|
||||
use crate::encode::{self, Codec};
|
||||
use anyhow::{Context, Result};
|
||||
use std::io::Write;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct HelperOptions {
|
||||
pub target_id: u32,
|
||||
@@ -24,11 +26,18 @@ pub struct HelperOptions {
|
||||
pub height: u32,
|
||||
pub fps: u32,
|
||||
pub bitrate_kbps: u32,
|
||||
/// Negotiated encode bit depth (8, or 10 = HEVC Main10). HDR auto-upgrades to 10 from the
|
||||
/// captured frame's `Rgb10a2` format regardless.
|
||||
pub bit_depth: u8,
|
||||
}
|
||||
|
||||
/// AU framing magic + version, so the host can resync / detect a helper crash on its stdout stream.
|
||||
const AU_MAGIC: u32 = 0x5046_4155; // "PFAU"
|
||||
|
||||
/// Control byte the host writes on our stdin to force the next frame to be an IDR. Must match
|
||||
/// `wgc_relay::CTL_KEYFRAME`.
|
||||
const CTL_KEYFRAME: u8 = 0x01;
|
||||
|
||||
pub fn run(opts: HelperOptions) -> Result<()> {
|
||||
tracing::info!(
|
||||
target_id = opts.target_id,
|
||||
@@ -59,11 +68,34 @@ pub fn run(opts: HelperOptions) -> Result<()> {
|
||||
h,
|
||||
opts.fps,
|
||||
opts.bitrate_kbps as u64 * 1000,
|
||||
false, // not cuda
|
||||
8, // bit depth: HDR auto-upgrades to Main10 from the Rgb10a2 frame
|
||||
false, // not cuda
|
||||
opts.bit_depth, // 8, or 10 = Main10 (HDR auto-upgrades from the Rgb10a2 frame regardless)
|
||||
)
|
||||
.context("open NVENC")?;
|
||||
|
||||
// Control channel: the host writes a single byte on our stdin to force an IDR (client decode
|
||||
// recovery), mirroring `enc.request_keyframe()` in the single-process path. A reader thread sets
|
||||
// a flag the encode loop checks; stdin EOF (host gone) just stops the thread.
|
||||
let kf = Arc::new(AtomicBool::new(false));
|
||||
{
|
||||
let kf = kf.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("wgc-helper-ctl".into())
|
||||
.spawn(move || {
|
||||
let mut stdin = std::io::stdin();
|
||||
let mut byte = [0u8; 1];
|
||||
while let Ok(n) = stdin.read(&mut byte) {
|
||||
if n == 0 {
|
||||
break; // host closed our stdin
|
||||
}
|
||||
if byte[0] == CTL_KEYFRAME {
|
||||
kf.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Binary stdout — lock it once + write framed AUs. A short write / broken pipe means the host
|
||||
// (parent) went away → exit cleanly so the host's relaunch watchdog can respawn us.
|
||||
let stdout = std::io::stdout();
|
||||
@@ -71,6 +103,9 @@ pub fn run(opts: HelperOptions) -> Result<()> {
|
||||
|
||||
let mut frame = first;
|
||||
loop {
|
||||
if kf.swap(false, Ordering::Relaxed) {
|
||||
enc.request_keyframe();
|
||||
}
|
||||
enc.submit(&frame).context("encoder submit")?;
|
||||
while let Some(au) = enc.poll().context("encoder poll")? {
|
||||
if write_au(&mut out, &au).is_err() {
|
||||
|
||||
Reference in New Issue
Block a user