From c23d9504761daca3611f8cad7b35ff98d95a5c20 Mon Sep 17 00:00:00 2001 From: Avalon Danvers Date: Mon, 20 Jul 2026 00:14:08 -0700 Subject: [PATCH] vgd-probe: accept multiple mode arguments; record phase-5 lifecycle milestone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parse_args kept a single Option, so "3456x2160@240 3456x2160@120" silently dropped every mode but the last and the multi-mode create path was untestable standalone. Collect all WxH@HZ arguments instead (default list unchanged when none given). CLAUDE.md: phase-5 lifecycle milestone verified — Moonlight streams off the virtual display via LuminalShine's luminalvgd backend (exclusive topology, 240 Hz multi-mode, clean restore). Records the host-side integration lessons (NBF-aware resolvers, millihertz units, SDR-until- HDR10) and scopes tranche 3b. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 32 ++++++++++++++++++++++++++++++++ crates/vgd-probe/src/main.rs | 15 ++++++++------- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b2f2d5c..d3fb708 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -174,3 +174,35 @@ AcquireBufferFailedExit etc. under the provider GUID above. Next: phase 5 (LuminalShine `luminalvgd` backend consuming the ring), then WGC-RELIABILITY §7 alttab_stress port, cursor/gamma/HDR DDIs. + +### Phase 5 — lifecycle backend COMPLETE (2026-07-20, Windows box) + +**Milestone verified: Moonlight client streams off the virtual +display** — LuminalShine (branch `feat/luminalvgd-backend`) auto-selects +the LuminalVGD backend, creates a per-client monitor (multi-mode: +framegen 240 Hz + base 120 Hz), the display helper applies the +exclusive topology (physical monitors off) at 240 Hz with APPLY acked +in ~1 s, WGC captures the virtual display at the client's native +3456×2160, and both physical monitors restore on session end. Capture +still goes through the WGC helper — the ring-consuming capture backend +is tranche 3b. + +Integration lessons (all host-side, none required driver changes): +- LuminalShine's display resolvers/predicates had to learn the NBF + vendor prefix and "Luminal Video Graphics Display" adapter name; the + driver-side identity scheme needed nothing. +- Mode-list units: the FFI takes millihertz. LuminalShine normalized to + mHz and then rescaled ×1000 — Windows silently discards a 240 kHz + mode, leaving only the base rate. The driver's ParseMonitorDescription2 + / QueryTargetModes2 paths were verified correct via vgd-probe + + EnumDisplaySettings (both modes register; preferred applies at 240 Hz). +- HDR: the host now requests SDR for VGD displays; asking Windows to + enable HDR on a monitor without HDR10 caps fails the entire + SetDisplayConfig apply. Driver HDR10 (EDID metadata + IddCx caps + + 10-bit ring formats) is the gating work for HDR streaming. +- vgd-probe now accepts multiple `WxH@HZ` args (previously the last + one silently won), so multi-mode creates are testable standalone. + +Next: tranche 3b — ring-consuming capture backend in LuminalShine +(`display_vgd` platf::display_t), driver HDR10 caps, cursor/gamma DDIs, +WGC-RELIABILITY §7 alttab_stress port. diff --git a/crates/vgd-probe/src/main.rs b/crates/vgd-probe/src/main.rs index aa02573..a816343 100644 --- a/crates/vgd-probe/src/main.rs +++ b/crates/vgd-probe/src/main.rs @@ -54,7 +54,7 @@ fn print_status(s: &GetStatusReply) { struct Args { status_only: bool, /// Explicit `WxH@HZ` from the command line; None = default mode list. - explicit_mode: Option, + explicit_modes: Vec, hold_secs: u64, /// Mint a throwaway identity: Windows treats the monitor as brand new /// (no remembered topology/disconnect state) — useful when the stable @@ -79,7 +79,7 @@ const DEFAULT_MODES: [ModeSpec; 3] = [ fn parse_args() -> Result { let mut args = Args { status_only: false, - explicit_mode: None, + explicit_modes: Vec::new(), hold_secs: 15, ephemeral: false, consume: false, @@ -99,7 +99,7 @@ fn parse_args() -> Result { let (dims, hz) = mode.split_once('@').unwrap_or((mode, "60")); let (w, h) = dims.split_once('x').ok_or_else(|| format!("bad mode: {mode}"))?; let hz: f64 = hz.parse().map_err(|_| format!("bad refresh: {hz}"))?; - args.explicit_mode = Some(ModeSpec { + args.explicit_modes.push(ModeSpec { width: w.parse().map_err(|_| format!("bad width: {w}"))?, height: h.parse().map_err(|_| format!("bad height: {h}"))?, refresh_millihz: (hz * 1000.0).round() as u32, @@ -116,7 +116,7 @@ fn main() -> ExitCode { Ok(a) => a, Err(e) => { eprintln!("vgd-probe: {e}"); - eprintln!("usage: vgd-probe [status] [WxH@HZ] [--hold SECS]"); + eprintln!("usage: vgd-probe [status] [WxH@HZ ...] [--hold SECS]"); return ExitCode::FAILURE; } }; @@ -161,9 +161,10 @@ fn main() -> ExitCode { .unwrap_or(1) | (std::process::id() as u64) << 48; - let mode_list: Vec = match args.explicit_mode { - Some(m) => vec![m], - None => DEFAULT_MODES.to_vec(), + let mode_list: Vec = if args.explicit_modes.is_empty() { + DEFAULT_MODES.to_vec() + } else { + args.explicit_modes.clone() }; let described: Vec = mode_list .iter()