fix(host): filter IddCx ghost adapters out of the Windows GPU inventory

pf-vdisplay's IddCx adapter enumerates as a perfect DXGI twin of the GPU it
renders on — same Description, VendorId/DeviceId/SubSysId, even
DedicatedVideoMemory; only the LUID differs (verified on the RTX box: the
twins were byte-identical in DXGI). Every host with the driver installed
therefore listed each render GPU twice: the console picker offered a dead
twin (display-only — D3D11 device creation on it fails), and auto-select's
max-VRAM tie between the twins was settled by enumeration order — the real
GPU won by luck, not construction.

The kernel adapter type is the only field that separates the twins, so
enumerate() now queries KMTQAITYPE_ADAPTERTYPE per adapter (raw gdi32 FFI —
the windows crate's Wdk bindings aren't enabled, and one 3-call query
doesn't justify them) and drops indirect-display, display-only, and software
adapters. Fail-open: a failed query keeps the adapter — better a listed twin
than a hidden real GPU. Incidentally also filters other vendors' virtual
displays (Parsec-VDD class) from user machines' pickers.

Existing manual preferences heal via find_preferred's same-model fallback
when occurrence indices shift — a ghost accidentally pinned as -0 now
resolves to the real GPU. The regression test pins the adapter-type words
captured from real hardware; a Windows-gated smoke test runs enumerate()
against the live machine (on the RTX box it actively exercises ghost
exclusion: the inventory went from twin 4090s to exactly one).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 20:42:42 +02:00
parent 5d9f1ccb6d
commit c19c87c435
2 changed files with 157 additions and 6 deletions
+154 -4
View File
@@ -2,9 +2,11 @@
//! //!
//! Three concerns, one module: //! Three concerns, one module:
//! - **Enumeration** ([`enumerate`]): the machine's hardware GPUs — DXGI adapters on Windows //! - **Enumeration** ([`enumerate`]): the machine's hardware GPUs — DXGI adapters on Windows
//! (WARP/Basic-Render filtered out), `/dev/dri/renderD*` + sysfs PCI ids on Linux, empty //! (WARP/Basic-Render and indirect-display/display-only adapters filtered out — an IddCx
//! elsewhere. Compiled on every platform so the management endpoints (and the checked-in //! adapter like our own pf-vdisplay mirrors its render GPU's whole DXGI identity and would
//! OpenAPI document) are identical everywhere. //! list every GPU twice), `/dev/dri/renderD*` + sysfs PCI ids on Linux, empty elsewhere.
//! Compiled on every platform so the management endpoints (and the checked-in OpenAPI
//! document) are identical everywhere.
//! - **Preference** ([`prefs`]): the operator's persisted auto/manual choice //! - **Preference** ([`prefs`]): the operator's persisted auto/manual choice
//! (`<config>/gpu-settings.json`, written by the mgmt API). A manual preference is stored by //! (`<config>/gpu-settings.json`, written by the mgmt API). A manual preference is stored by
//! *stable identity* — PCI vendor:device + occurrence + name — NOT by LUID (Windows LUIDs are //! *stable identity* — PCI vendor:device + occurrence + name — NOT by LUID (Windows LUIDs are
@@ -108,8 +110,100 @@ fn assign_ids(gpus: &mut [GpuInfo]) {
// Enumeration // Enumeration
// --------------------------------------------------------------------------------------------- // ---------------------------------------------------------------------------------------------
/// Which kernel adapter types ([`D3DKMT_ADAPTERTYPE`] bit-field words) never belong in the GPU
/// inventory. Pure bit math on every platform so the classification is unit-tested with words
/// captured from real hardware; only the Windows [`enumerate`] consumes it at runtime.
///
/// [`D3DKMT_ADAPTERTYPE`]: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/d3dkmdt/ns-d3dkmdt-_d3dkmt_adaptertype
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
mod adapter_type {
/// `RenderSupported` — clear on display-only adapters (an IDD ghost has no render engine).
const RENDER_SUPPORTED: u32 = 1 << 0;
/// `SoftwareDevice` — WARP/Basic Render (normally already dropped via the DXGI flag).
const SOFTWARE_DEVICE: u32 = 1 << 2;
/// `IndirectDisplayDevice` — an IddCx virtual-display adapter (pf-vdisplay, Parsec VDD, …).
const INDIRECT_DISPLAY_DEVICE: u32 = 1 << 6;
/// True when these bits describe an adapter that can never be the render/encode GPU:
/// indirect-display, software, or anything without render support.
pub(crate) fn hidden(bits: u32) -> bool {
bits & INDIRECT_DISPLAY_DEVICE != 0
|| bits & SOFTWARE_DEVICE != 0
|| bits & RENDER_SUPPORTED == 0
}
}
/// Kernel-side adapter-type query (`D3DKMTQueryAdapterInfo(KMTQAITYPE_ADAPTERTYPE)`) via raw
/// gdi32 FFI — the `windows` crate's `Wdk_*` bindings aren't enabled, and one 3-call query
/// doesn't justify them.
#[cfg(target_os = "windows")]
mod kmt {
/// `KMTQUERYADAPTERINFOTYPE::KMTQAITYPE_ADAPTERTYPE` (d3dkmdt.h).
const KMTQAITYPE_ADAPTERTYPE: u32 = 15;
/// `D3DKMT_OPENADAPTERFROMLUID`: LUID in, kernel adapter handle out.
#[repr(C)]
struct OpenAdapterFromLuid {
luid_low: u32,
luid_high: i32,
adapter: u32,
}
/// `D3DKMT_QUERYADAPTERINFO`.
#[repr(C)]
struct QueryAdapterInfo {
adapter: u32,
ty: u32,
private_data: *mut core::ffi::c_void,
private_data_size: u32,
}
/// `D3DKMT_CLOSEADAPTER`.
#[repr(C)]
struct CloseAdapter {
adapter: u32,
}
#[link(name = "gdi32")]
extern "system" {
fn D3DKMTOpenAdapterFromLuid(arg: *mut OpenAdapterFromLuid) -> i32;
fn D3DKMTQueryAdapterInfo(arg: *mut QueryAdapterInfo) -> i32;
fn D3DKMTCloseAdapter(arg: *mut CloseAdapter) -> i32;
}
/// The `D3DKMT_ADAPTERTYPE` bits for the adapter with this LUID, `None` when the kernel
/// query fails (callers fail open — better a listed twin than a hidden real GPU).
pub(crate) fn adapter_type_bits(luid_low: u32, luid_high: i32) -> Option<u32> {
// SAFETY: every pointer handed to the three D3DKMT calls addresses a stack local that
// outlives the call; NTSTATUS >= 0 is success. The kernel handle is closed on every
// path that opened it, including a failed query.
unsafe {
let mut open = OpenAdapterFromLuid {
luid_low,
luid_high,
adapter: 0,
};
if D3DKMTOpenAdapterFromLuid(&mut open) < 0 {
return None;
}
let mut bits: u32 = 0;
let mut query = QueryAdapterInfo {
adapter: open.adapter,
ty: KMTQAITYPE_ADAPTERTYPE,
private_data: (&mut bits as *mut u32).cast(),
private_data_size: size_of::<u32>() as u32,
};
let status = D3DKMTQueryAdapterInfo(&mut query);
let mut close = CloseAdapter {
adapter: open.adapter,
};
let _ = D3DKMTCloseAdapter(&mut close);
(status >= 0).then_some(bits)
}
}
}
/// Enumerate this host's hardware GPUs. Windows: DXGI adapters minus WARP/Basic-Render (the same /// Enumerate this host's hardware GPUs. Windows: DXGI adapters minus WARP/Basic-Render (the same
/// filter `win_adapter` always applied). Linux: `/dev/dri/renderD*` with PCI ids from sysfs. /// filter `win_adapter` always applied) and minus indirect-display/display-only adapters (see the
/// ghost-twin filter inside). Linux: `/dev/dri/renderD*` with PCI ids from sysfs.
/// Other platforms (the macOS dev/test host build): empty — the endpoints still exist, they just /// Other platforms (the macOS dev/test host build): empty — the endpoints still exist, they just
/// report no GPUs. /// report no GPUs.
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
@@ -140,6 +234,26 @@ pub(crate) fn enumerate() -> Vec<GpuInfo> {
if lname.contains("basic render") || lname.contains("warp") { if lname.contains("basic render") || lname.contains("warp") {
continue; continue;
} }
// pf-vdisplay's IddCx adapter enumerates as a perfect DXGI twin of the GPU it renders
// on — same Description, VendorId/DeviceId/SubSysId, even DedicatedVideoMemory; only
// the LUID differs (verified on an RTX 4090 host). Left in, every punktfunk host
// lists each render GPU twice: the picker offers a dead twin (display-only — D3D11
// device creation on it fails) and auto-select's max-VRAM tie between the twins is
// settled by enumeration order. The kernel adapter type is the only field that
// separates them, so drop indirect-display (any vendor's virtual display, ours
// included), display-only, and software adapters here. A failed query keeps the
// adapter (fail open).
match kmt::adapter_type_bits(d.AdapterLuid.LowPart, d.AdapterLuid.HighPart) {
Some(bits) if adapter_type::hidden(bits) => {
tracing::debug!(
name = %name,
bits = %format!("{bits:#x}"),
"skipping non-render DXGI adapter (indirect display / display-only / software)"
);
continue;
}
_ => {}
}
out.push(GpuInfo { out.push(GpuInfo {
id: String::new(), id: String::new(),
name, name,
@@ -754,4 +868,40 @@ mod tests {
drop(b); drop(b);
assert_eq!(active().unwrap().1, 0); // idle, last-used retained assert_eq!(active().unwrap().1, 0); // idle, last-used retained
} }
/// `D3DKMT_ADAPTERTYPE` words captured on a real host (RTX 4090 + pf-vdisplay + Raphael
/// iGPU, 2026-07): the IDD ghost and Basic Render hide, the real GPUs stay.
#[test]
fn adapter_type_hides_idd_ghost_keeps_real_gpus() {
assert!(adapter_type::hidden(0x0342)); // pf-vdisplay ghost twin: indirect + display-only
assert!(!adapter_type::hidden(0x031b)); // NVIDIA GeForce RTX 4090
assert!(!adapter_type::hidden(0x2323)); // AMD Radeon(TM) Graphics (Raphael iGPU)
assert!(adapter_type::hidden(0x0105)); // Microsoft Basic Render Driver (software)
}
/// End-to-end smoke of the ghost-twin filter + the raw D3DKMT FFI (struct layout, linking)
/// on whatever GPUs this Windows machine has: nothing the filter would hide may survive
/// [`enumerate`]. On a host with pf-vdisplay installed this actively exercises ghost
/// exclusion; on a GPU-less CI runner it passes vacuously.
#[cfg(target_os = "windows")]
#[test]
fn enumerate_excludes_non_render_adapters() {
for g in enumerate() {
let bits = kmt::adapter_type_bits(g.handle.luid_low, g.handle.luid_high);
eprintln!(
"enumerated: {} ({}) kmt_bits={}",
g.name,
g.id,
bits.map_or("<query failed>".into(), |b| format!("{b:#x}")),
);
if let Some(bits) = bits {
assert!(
!adapter_type::hidden(bits),
"{} ({}) should have been filtered (bits {bits:#x})",
g.name,
g.id
);
}
}
}
} }
@@ -8,8 +8,9 @@
//! //!
//! The selection logic itself now lives in [`crate::gpu`] (shared with the mgmt API's GPU //! The selection logic itself now lives in [`crate::gpu`] (shared with the mgmt API's GPU
//! endpoints): **operator preference (web console) > `PUNKTFUNK_RENDER_ADAPTER` substring > max //! endpoints): **operator preference (web console) > `PUNKTFUNK_RENDER_ADAPTER` substring > max
//! `DedicatedVideoMemory`**, WARP/Basic-Render always excluded. This wrapper is the LUID-shaped //! `DedicatedVideoMemory`**, WARP/Basic-Render and indirect-display ghost twins always excluded.
//! view of it, plus the per-decision logging (call sites are per-session, never per-frame). //! This wrapper is the LUID-shaped view of it, plus the per-decision logging (call sites are
//! per-session, never per-frame).
use windows::Win32::Foundation::LUID; use windows::Win32::Foundation::LUID;