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 pipeline;
|
||||||
mod pwinit;
|
mod pwinit;
|
||||||
mod vdisplay;
|
mod vdisplay;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
mod wgc_helper;
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
mod zerocopy;
|
mod zerocopy;
|
||||||
|
|
||||||
@@ -195,6 +197,34 @@ fn real_main() -> Result<()> {
|
|||||||
paired_store: None,
|
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 => {
|
Some("-h") | Some("--help") | Some("help") | None => {
|
||||||
print_usage();
|
print_usage();
|
||||||
Ok(())
|
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