Files
punktfunk/clients/windows/src/gpu.rs
T
enricobuehlerandClaude Fable 5 698925a036 fix(clients): settings saves stop reverting each other, Windows gets audio pickers, and a game link launches its game
The rest of the client-gaps handoff, plus what a parity sweep surfaced:

* Every whole-file settings save rebases on the file first — the GTK
  dialog close and its two speed-test apply arms, the Windows
  per-control commit, the console screen. The file has five writers and
  no merge (profiles.rs documents the debt); saving a shell-lifetime
  snapshot silently reverted whatever another writer stored meanwhile,
  most visibly the spawner-persisted match-window size (item 4).
* Windows honors the Speaker/Microphone picks: `audio_wasapi` grows
  endpoint enumeration (on its own MTA thread — UI threads are STA) and
  resolves PUNKTFUNK_AUDIO_SINK/SOURCE as endpoint ids, falling back to
  the default when the picked device is gone; the settings page gets
  the two rows (defaults scope, "(not detected)" like the GPU row).
  `speaker_device`/`mic_device` stop being Linux-only fields (item 5).
* A punktfunk:// link with `launch=` opened a plain desktop session on
  Windows — the id was parsed, validated, planned, and dropped at the
  last hop. It now rides `initiate_launch` (plus a waking variant for
  the dial-first path). Linux always forwarded it.
* The console UI offered "VAAPI" on Windows — a dead option there that
  also hid d3d11va, the actual Windows hardware path. The decoder list
  is per-OS now.
* The Windows gamepad picker learns "Steam Deck" (the GTK picker had
  it; the host-side pad has always existed).
* Doc fixes: gpu.rs named a nonexistent env var (PUNKTFUNK_ADAPTER →
  PUNKTFUNK_VK_ADAPTER), and session/Cargo.toml claimed video decode is
  Linux-only while explaining what the ARM64 leg drops.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 21:21:26 +02:00

95 lines
4.3 KiB
Rust

//! DXGI adapter enumeration for the Settings "GPU" picker.
//!
//! Streaming (decode + present) runs in the spawned `punktfunk-session` binary; the shell only
//! needs the list of real (hardware) adapters to offer on a multi-GPU box (a hybrid laptop or an
//! eGPU). The picked adapter description is persisted (`crate::trust::Settings::adapter`) and read
//! by the session child at connect (`PUNKTFUNK_VK_ADAPTER` remains the session binary's env
//! override).
use windows::core::Interface;
use windows::Win32::dxgi::{CreateDXGIFactory1, IDXGIAdapter, IDXGIFactory1};
/// The adapter's human-readable description.
fn adapter_name(adapter: &IDXGIAdapter) -> String {
// SAFETY: a read-only COM call on the live `adapter` borrow, filling a zeroed local
// descriptor through the out-param, checked before the descriptor is read; `&IDXGIAdapter`
// is a reference-counted wrapper, so the borrow IS the liveness.
unsafe {
let mut d: windows::Win32::dxgi::DXGI_ADAPTER_DESC = std::mem::zeroed();
if adapter.GetDesc(&mut d).is_ok() {
String::from_utf16_lossy(&d.Description)
.trim_end_matches('\0')
.to_string()
} else {
"<unknown adapter>".into()
}
}
}
/// Every DXGI adapter, in enumeration order.
fn all_adapters() -> Vec<IDXGIAdapter> {
// SAFETY: DXGI factory creation takes no pointer and returns an owned factory or an error,
// matched on below before anything uses it.
let factory: IDXGIFactory1 = match unsafe { CreateDXGIFactory1() } {
Ok(f) => f,
Err(_) => return Vec::new(),
};
let mut v = Vec::new();
let mut i = 0u32;
// SAFETY: a COM call on the live factory above; it takes an index and yields an owned adapter,
// and the `Ok` pattern is what proves one came back.
while let Ok(a) = unsafe { factory.EnumAdapters1(i) } {
i += 1;
if let Ok(a) = a.cast::<IDXGIAdapter>() {
v.push(a);
}
}
v
}
/// Descriptions of the real (hardware, non-WARP) GPUs — the Settings GPU picker's option list.
/// The picker only shows when this has more than one entry.
///
/// **Deduplicated by description**, because the description IS the identity everywhere
/// downstream: the pick is persisted as that string (`Settings::adapter`) and matched by
/// name in the session binary (`PUNKTFUNK_VK_ADAPTER`). So two entries with the same name
/// are one selectable choice however many times DXGI enumerates them — listing it twice
/// only offers the user a meaningless coin flip. Seen live on an Intel Arc laptop
/// (2026-07-19), whose Vulkan ICD likewise enumerates the one physical iGPU twice.
pub fn adapter_names() -> Vec<String> {
const DXGI_ADAPTER_FLAG_SOFTWARE: u32 = 2; // dxgi.h; not in this windows-rs feature set
let mut names: Vec<String> = Vec::new();
for a in all_adapters() {
let desc1 = a
.cast::<windows::Win32::dxgi::IDXGIAdapter1>()
.ok()
// SAFETY: a read-only COM call on the adapter just cast, filling a zeroed local
// descriptor through the out-param, discarded unless the call reports success.
.and_then(|a1| unsafe {
let mut d: windows::Win32::dxgi::DXGI_ADAPTER_DESC1 = std::mem::zeroed();
a1.GetDesc1(&mut d).is_ok().then_some(d)
});
let name = adapter_name(&a);
// Forensics for the next duplicate/oddity report — which adapters DXGI actually
// returned, and whether the repeats share a LUID (one adapter enumerated twice)
// or are distinct devices that merely present the same description.
if let Some(d) = &desc1 {
tracing::debug!(
name = %name,
luid = format!("{:08x}-{:08x}", d.AdapterLuid.HighPart, d.AdapterLuid.LowPart),
vendor = format_args!("{:#06x}", d.VendorId),
device = format_args!("{:#06x}", d.DeviceId),
flags = d.Flags,
"DXGI adapter"
);
}
if desc1.is_some_and(|d| d.Flags & DXGI_ADAPTER_FLAG_SOFTWARE != 0) {
continue; // WARP / software renderer — never a streaming target
}
if !names.contains(&name) {
names.push(name);
}
}
names
}