feat(host/windows): WGC helper subcommand (two-process secure-desktop, step 3)
`m3-host wgc-helper --target-id N --gdi NAME --mode WxHxHz --bitrate K`: the USER-session half of the two-process secure-desktop design (docs/windows-secure-desktop.md). Opens WGC on the EXISTING SudoVDA output by GDI name only (never creates a virtual output — a second topology owner re-trips the ACCESS_LOST born-lost storm), encodes via NVENC, and ships framed Annex-B AUs on stdout for the SYSTEM host to relay onto the live QUIC session: `[u32 magic "PFAU"][u32 len][u64 pts_ns][u8 keyframe][data]`. tracing → stderr so stdout stays the pure AU stream. cfg-gated windows-only; Linux build unaffected. scripts/headless/win-build.cmd: the canonical box build script (sets PUNKTFUNK_BUILD_VERSION so build.rs stamps the version + the NVENC LIB path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -32,6 +32,8 @@ mod native_pairing;
|
||||
mod pipeline;
|
||||
mod pwinit;
|
||||
mod vdisplay;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod wgc_helper;
|
||||
#[cfg(target_os = "linux")]
|
||||
mod zerocopy;
|
||||
|
||||
@@ -195,6 +197,34 @@ fn real_main() -> Result<()> {
|
||||
paired_store: None,
|
||||
})
|
||||
}
|
||||
// USER-session WGC helper (Windows two-process secure-desktop design): capture the EXISTING
|
||||
// SudoVDA via WGC + NVENC, stream AUs on stdout to the SYSTEM host. Spawned by the host
|
||||
// (CreateProcessAsUser), not run by hand. See docs/windows-secure-desktop.md.
|
||||
#[cfg(target_os = "windows")]
|
||||
Some("wgc-helper") => {
|
||||
let get = |flag: &str| {
|
||||
args.iter()
|
||||
.skip_while(|a| *a != flag)
|
||||
.nth(1)
|
||||
.map(String::as_str)
|
||||
};
|
||||
let (width, height, fps) = get("--mode")
|
||||
.and_then(|m| {
|
||||
let p: Vec<u32> = m.split('x').filter_map(|s| s.parse().ok()).collect();
|
||||
(p.len() == 3).then(|| (p[0], p[1], p[2]))
|
||||
})
|
||||
.unwrap_or((1920, 1080, 60));
|
||||
wgc_helper::run(wgc_helper::HelperOptions {
|
||||
target_id: get("--target-id").and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||
gdi_name: get("--gdi").unwrap_or("").to_string(),
|
||||
width,
|
||||
height,
|
||||
fps,
|
||||
bitrate_kbps: get("--bitrate")
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(20000),
|
||||
})
|
||||
}
|
||||
Some("-h") | Some("--help") | Some("help") | None => {
|
||||
print_usage();
|
||||
Ok(())
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
//! USER-session WGC helper (Windows) — part of the two-process secure-desktop design
|
||||
//! (docs/windows-secure-desktop.md).
|
||||
//!
|
||||
//! WGC won't activate under the SYSTEM account, but the host must run as SYSTEM for the secure
|
||||
//! desktop. So the SYSTEM host spawns THIS helper in the interactive user session
|
||||
//! (`CreateProcessAsUserW`) to do the WGC capture + NVENC encode that needs the user token, and the
|
||||
//! helper ships the encoded Annex-B access units back over its **stdout** pipe (which the host
|
||||
//! inherits + reads). The host relays them on the live QUIC session while the normal desktop is up,
|
||||
//! and switches to its own DDA encoder on the secure desktop. The helper captures the SAME SudoVDA
|
||||
//! output **by GDI name only** — it never creates a virtual output / touches display topology (a
|
||||
//! second topology owner would re-trigger the ACCESS_LOST born-lost storm).
|
||||
//!
|
||||
//! Wire framing on stdout, per AU: `[u32 len LE][u64 pts_ns LE][u8 keyframe][len bytes data]`.
|
||||
|
||||
use crate::capture::{dxgi::WinCaptureTarget, wgc::WgcCapturer, Capturer};
|
||||
use crate::encode::{self, Codec};
|
||||
use anyhow::{Context, Result};
|
||||
use std::io::Write;
|
||||
|
||||
pub struct HelperOptions {
|
||||
pub target_id: u32,
|
||||
pub gdi_name: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub fps: u32,
|
||||
pub bitrate_kbps: u32,
|
||||
}
|
||||
|
||||
/// AU framing magic + version, so the host can resync / detect a helper crash on its stdout stream.
|
||||
const AU_MAGIC: u32 = 0x5046_4155; // "PFAU"
|
||||
|
||||
pub fn run(opts: HelperOptions) -> Result<()> {
|
||||
tracing::info!(
|
||||
target_id = opts.target_id,
|
||||
gdi = %opts.gdi_name,
|
||||
mode = format!("{}x{}@{}", opts.width, opts.height, opts.fps),
|
||||
"WGC helper starting (user session)"
|
||||
);
|
||||
|
||||
// Capture the EXISTING SudoVDA output by GDI name / target id — do NOT create one (the host owns
|
||||
// the virtual output + its isolate/restore; a second topology owner breaks DDA recovery).
|
||||
let target = WinCaptureTarget {
|
||||
adapter_luid: 0,
|
||||
gdi_name: opts.gdi_name.clone(),
|
||||
target_id: opts.target_id,
|
||||
};
|
||||
let mut cap =
|
||||
WgcCapturer::open(target, Some((opts.width, opts.height, opts.fps))).context("WGC open")?;
|
||||
cap.set_active(true);
|
||||
|
||||
// First frame establishes the real dimensions + whether the desktop is HDR (the encoder derives
|
||||
// Main10/HDR from the frame's PixelFormat::Rgb10a2). Then open NVENC on the capture device.
|
||||
let first = cap.next_frame().context("first WGC frame")?;
|
||||
let (w, h) = (first.width, first.height);
|
||||
let mut enc = encode::open_video(
|
||||
Codec::H265,
|
||||
first.format,
|
||||
w,
|
||||
h,
|
||||
opts.fps,
|
||||
opts.bitrate_kbps as u64 * 1000,
|
||||
false, // not cuda
|
||||
8, // bit depth: HDR auto-upgrades to Main10 from the Rgb10a2 frame
|
||||
)
|
||||
.context("open NVENC")?;
|
||||
|
||||
// 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();
|
||||
let mut out = stdout.lock();
|
||||
|
||||
let mut frame = first;
|
||||
loop {
|
||||
enc.submit(&frame).context("encoder submit")?;
|
||||
while let Some(au) = enc.poll().context("encoder poll")? {
|
||||
if write_au(&mut out, &au).is_err() {
|
||||
tracing::info!("WGC helper: stdout closed (host gone) — exiting");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
frame = cap.next_frame().context("WGC next frame")?;
|
||||
}
|
||||
}
|
||||
|
||||
fn write_au(out: &mut impl Write, au: &encode::EncodedFrame) -> std::io::Result<()> {
|
||||
out.write_all(&AU_MAGIC.to_le_bytes())?;
|
||||
out.write_all(&(au.data.len() as u32).to_le_bytes())?;
|
||||
out.write_all(&au.pts_ns.to_le_bytes())?;
|
||||
out.write_all(&[au.keyframe as u8])?;
|
||||
out.write_all(&au.data)?;
|
||||
out.flush()
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
@echo off
|
||||
call "C:\Program Files\Microsoft Visual Studio\18\Community\VC\Auxiliary\Build\vcvars64.bat" >nul 2>&1
|
||||
set "LIB=%LIB%;C:\Users\Public\nvenc"
|
||||
set "PATH=%USERPROFILE%\.cargo\bin;%PATH%"
|
||||
set "PUNKTFUNK_BUILD_VERSION=0.2.0-win-dev"
|
||||
cd /d C:\Users\Public\punktfunk-native
|
||||
cargo build -r -p punktfunk-host --features nvenc 2>&1
|
||||
echo BUILD_EXIT=%ERRORLEVEL%
|
||||
Reference in New Issue
Block a user