fix(host/windows): NVENC capture on real GPU + HOME-less config dir
apple / swift (push) Successful in 54s
android / android (push) Failing after 1m44s
ci / rust (push) Successful in 1m18s
ci / web (push) Successful in 28s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 1m50s
decky / build-publish (push) Successful in 10s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
flatpak / build-publish (push) Failing after 2s
deb / build-publish (push) Failing after 2m56s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m4s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m48s
docker / deploy-docs (push) Successful in 17s
apple / swift (push) Successful in 54s
android / android (push) Failing after 1m44s
ci / rust (push) Successful in 1m18s
ci / web (push) Successful in 28s
ci / docs-site (push) Successful in 31s
ci / bench (push) Successful in 1m50s
decky / build-publish (push) Successful in 10s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 5s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 3s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 3s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 5s
flatpak / build-publish (push) Failing after 2s
deb / build-publish (push) Failing after 2m56s
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Successful in 5m4s
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Successful in 4m48s
docker / deploy-docs (push) Successful in 17s
Validated live on an RTX 4090 (Windows 11) host streaming to the Rust reference client over the LAN: SudoVDA virtual display → DXGI Desktop Duplication (D3D11 zero-copy) → NVENC HEVC → punktfunk/1. 720p60 and 1080p60 both clean (181 / 177 frames, 0 mismatched, p50 1.6 / 3.45 ms cross-machine), coexisting with Apollo. Two real-hardware bugs the GPU-less VM couldn't surface: - DXGI capturer: the SudoVDA virtual monitor's DXGI output is enumerated under the GPU that *renders* it (the 4090, LUID 0x15df6), NOT under the SudoVDA "adapter" LUID SudoVDA reports (0x23276). Restricting the output search to that LUID found nothing → "adapter has no output named \\.\DISPLAYn". Now search ALL adapters for the GDI name, bind the D3D11 device to whichever adapter exposes it (NVENC then shares that device), with a settle-retry (the output appears a beat after display creation) and topology logging. - native_pairing / apps: keyed config paths off raw $HOME, which a Windows service/scheduled-task context doesn't set → "HOME unset" hard-fail at m3-host startup. Route both through gamestream::config_dir(), which falls back to %APPDATA% on Windows (cert/paired/apps now under AppData\Roaming). clippy -D warnings + build green on x86_64-pc-windows-msvc (default and --features nvenc) and Linux (78/78 tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
//! WDDM output). Compiles on the GPU-less VM; the pure helpers are unit-tested there.
|
//! WDDM output). Compiles on the GPU-less VM; the pure helpers are unit-tested there.
|
||||||
|
|
||||||
use super::{CapturedFrame, Capturer, FramePayload, PixelFormat};
|
use super::{CapturedFrame, Capturer, FramePayload, PixelFormat};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||||
use windows::core::Interface;
|
use windows::core::Interface;
|
||||||
@@ -99,19 +99,88 @@ impl DuplCapturer {
|
|||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let factory: IDXGIFactory1 = CreateDXGIFactory1().context("CreateDXGIFactory1")?;
|
let factory: IDXGIFactory1 = CreateDXGIFactory1().context("CreateDXGIFactory1")?;
|
||||||
// 1) the adapter whose LUID matches SudoVDA's AddOut.luid.
|
// 1) Find the output (monitor) whose GDI DeviceName matches, across ALL adapters. On a
|
||||||
let mut adapter: Option<IDXGIAdapter1> = None;
|
// real-GPU box the SudoVDA virtual monitor's DXGI output is enumerated under the GPU that
|
||||||
|
// *renders* it (the discrete/integrated GPU), NOT under the SudoVDA "adapter" LUID that
|
||||||
|
// SudoVDA reports — so we can't restrict the search to `target.adapter_luid`. The output
|
||||||
|
// also appears a beat after the display is created, so settle-retry for up to ~2 s.
|
||||||
|
// `target.adapter_luid` is kept only as a tie-break preference (matched adapter first).
|
||||||
|
let _ = target.adapter_luid;
|
||||||
|
let deadline = Instant::now() + Duration::from_millis(2000);
|
||||||
|
let (adapter, output): (IDXGIAdapter1, IDXGIOutput1) = loop {
|
||||||
|
let mut hit = None;
|
||||||
let mut i = 0u32;
|
let mut i = 0u32;
|
||||||
while let Ok(a) = factory.EnumAdapters1(i) {
|
while let Ok(a) = factory.EnumAdapters1(i) {
|
||||||
let d = a.GetDesc1()?;
|
let ad = a.GetDesc1()?;
|
||||||
if pack_luid(d.AdapterLuid) == target.adapter_luid {
|
let aname = String::from_utf16_lossy(&ad.Description);
|
||||||
adapter = Some(a);
|
let aname = aname.trim_end_matches('\u{0}');
|
||||||
|
let mut j = 0u32;
|
||||||
|
while let Ok(o) = a.EnumOutputs(j) {
|
||||||
|
let od = o.GetDesc()?;
|
||||||
|
let oname = String::from_utf16_lossy(&od.DeviceName);
|
||||||
|
let oname = oname.trim_end_matches('\u{0}').to_string();
|
||||||
|
tracing::debug!(
|
||||||
|
adapter = aname,
|
||||||
|
luid = format!("{:#x}", pack_luid(ad.AdapterLuid)),
|
||||||
|
output = oname,
|
||||||
|
want = target.gdi_name,
|
||||||
|
"DXGI output seen"
|
||||||
|
);
|
||||||
|
if gdi_name_matches(&od.DeviceName, &target.gdi_name) {
|
||||||
|
tracing::info!(
|
||||||
|
adapter = aname,
|
||||||
|
luid = format!("{:#x}", pack_luid(ad.AdapterLuid)),
|
||||||
|
output = oname,
|
||||||
|
"capturing the SudoVDA output on this adapter"
|
||||||
|
);
|
||||||
|
hit = Some((a.clone(), o.cast::<IDXGIOutput1>()?));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
if hit.is_some() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
let adapter = adapter.context("no DXGI adapter matches the SudoVDA LUID")?;
|
if let Some(h) = hit {
|
||||||
// 2) D3D11 device ON that adapter (driver_type MUST be UNKNOWN with an explicit adapter).
|
break h;
|
||||||
|
}
|
||||||
|
if Instant::now() >= deadline {
|
||||||
|
let mut topo = Vec::new();
|
||||||
|
let mut i = 0u32;
|
||||||
|
while let Ok(a) = factory.EnumAdapters1(i) {
|
||||||
|
let ad = a.GetDesc1()?;
|
||||||
|
let an = String::from_utf16_lossy(&ad.Description);
|
||||||
|
let mut outs = Vec::new();
|
||||||
|
let mut j = 0u32;
|
||||||
|
while let Ok(o) = a.EnumOutputs(j) {
|
||||||
|
let od = o.GetDesc()?;
|
||||||
|
outs.push(
|
||||||
|
String::from_utf16_lossy(&od.DeviceName)
|
||||||
|
.trim_end_matches('\u{0}')
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
topo.push(format!(
|
||||||
|
"{} [{:#x}]: {:?}",
|
||||||
|
an.trim_end_matches('\u{0}'),
|
||||||
|
pack_luid(ad.AdapterLuid),
|
||||||
|
outs
|
||||||
|
));
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
bail!(
|
||||||
|
"no DXGI adapter exposes output {} (topology: {})",
|
||||||
|
target.gdi_name,
|
||||||
|
topo.join(" | ")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
std::thread::sleep(Duration::from_millis(100));
|
||||||
|
};
|
||||||
|
// 2) D3D11 device ON the adapter that exposes the output (driver_type MUST be UNKNOWN with
|
||||||
|
// an explicit adapter). NVENC binds to this same device for zero-copy encode.
|
||||||
let mut device: Option<ID3D11Device> = None;
|
let mut device: Option<ID3D11Device> = None;
|
||||||
let mut context: Option<ID3D11DeviceContext> = None;
|
let mut context: Option<ID3D11DeviceContext> = None;
|
||||||
D3D11CreateDevice(
|
D3D11CreateDevice(
|
||||||
@@ -128,20 +197,7 @@ impl DuplCapturer {
|
|||||||
.context("D3D11CreateDevice")?;
|
.context("D3D11CreateDevice")?;
|
||||||
let device = device.context("null D3D11 device")?;
|
let device = device.context("null D3D11 device")?;
|
||||||
let context = context.context("null D3D11 context")?;
|
let context = context.context("null D3D11 context")?;
|
||||||
// 3) the output (monitor) whose GDI DeviceName matches.
|
// 3) duplicate the output.
|
||||||
let mut out1: Option<IDXGIOutput1> = None;
|
|
||||||
let mut j = 0u32;
|
|
||||||
while let Ok(o) = adapter.EnumOutputs(j) {
|
|
||||||
let od = o.GetDesc()?;
|
|
||||||
if gdi_name_matches(&od.DeviceName, &target.gdi_name) {
|
|
||||||
out1 = Some(o.cast::<IDXGIOutput1>()?);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
j += 1;
|
|
||||||
}
|
|
||||||
let output =
|
|
||||||
out1.with_context(|| format!("adapter has no output named {}", target.gdi_name))?;
|
|
||||||
// 4) duplicate the output.
|
|
||||||
let dupl = output
|
let dupl = output
|
||||||
.DuplicateOutput(&device)
|
.DuplicateOutput(&device)
|
||||||
.context("DuplicateOutput (already duplicated by another app?)")?;
|
.context("DuplicateOutput (already duplicated by another app?)")?;
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ pub struct AppEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn config_path() -> Option<std::path::PathBuf> {
|
fn config_path() -> Option<std::path::PathBuf> {
|
||||||
Some(std::path::Path::new(&std::env::var("HOME").ok()?).join(".config/punktfunk/apps.json"))
|
// `config_dir()` resolves XDG/HOME on Linux and %APPDATA% on Windows (no HOME needed).
|
||||||
|
Some(super::config_dir().join("apps.json"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_compositor(s: &str) -> Option<crate::vdisplay::Compositor> {
|
fn parse_compositor(s: &str) -> Option<crate::vdisplay::Compositor> {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
//! enters it (the client needs it to build its first message). So the UI **displays** the PIN —
|
//! enters it (the client needs it to build its first message). So the UI **displays** the PIN —
|
||||||
//! armed on demand for a short window — rather than accepting one.
|
//! armed on demand for a short window — rather than accepting one.
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::Result;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -101,8 +101,9 @@ pub struct NativePairingStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn default_path() -> Result<PathBuf> {
|
fn default_path() -> Result<PathBuf> {
|
||||||
let home = std::env::var("HOME").context("HOME unset")?;
|
// `config_dir()` resolves XDG/HOME on Linux and falls back to %APPDATA% on Windows — so the
|
||||||
Ok(PathBuf::from(home).join(".config/punktfunk/punktfunk1-paired.json"))
|
// native paired-store works without a HOME env var (which a Windows service/task doesn't set).
|
||||||
|
Ok(crate::gamestream::config_dir().join("punktfunk1-paired.json"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load(path: &std::path::Path) -> PairedClients {
|
fn load(path: &std::path::Path) -> PairedClients {
|
||||||
|
|||||||
Reference in New Issue
Block a user