fix(client): the probe printed a device index the env var does not take

--probe-decode printed its DISPLAY position and called it the
PUNKTFUNK_VK_DEVICE value. It is not. pick_device resolves that variable against
the RAW vkEnumeratePhysicalDevices order (setup.rs, `devices.get(i)`) BEFORE any
ranking runs, while the probe sorts discrete-first for readability.

Those two orders disagree precisely on the hardware this flag exists to
diagnose. pick_device's own comment records why the ranking is there: "enumeration
order puts the iGPU FIRST on some hybrids (observed: Ryzen iGPU ahead of an RTX
dGPU)". So on a hybrid laptop the number the probe printed for the iGPU could
well be the number for the dGPU — a diagnostic handing out an actionable value
that selects the other GPU, which is worse than printing none.

Measured on the Arc + RTX 3500 Ada laptop, which is also where the first output
went out with the wrong claim in it: three adapters, and the same Arc iGPU
enumerated TWICE. So AdapterDecode now carries the raw enumeration index,
captured before the sort, and the printer uses it; the "default presenter"
marker stays on the first LISTED entry, because sorted-first is what pick_device
lands on when nothing overrides.

The duplicate is why the trailing hint names PUNKTFUNK_VK_ADAPTER as the safer
knob and admits its limit: two adapters sharing a marketing name cannot be told
apart by it, and a name match resolves to whichever enumerates first. The hint
also states the thing this whole output invites a reader to get wrong — that a
capable GPU in the list does not mean the decoder will use it, because Vulkan
Video decodes on the presenter's device and PUNKTFUNK_DECODER does not move the
presenter.

Gates: fmt clean; clippy -D warnings on punktfunk-client-session and pf-presenter.
This commit is contained in:
2026-08-07 13:52:23 +02:00
parent 43e713ecca
commit c0f8f051c3
2 changed files with 48 additions and 4 deletions
+31 -2
View File
@@ -548,12 +548,21 @@ mod session_main {
println!("no Vulkan physical devices");
}
for (i, a) in adapters.iter().enumerate() {
// The index IS the PUNKTFUNK_VK_DEVICE value, and entry 0 is what
// The bracketed number is the PUNKTFUNK_VK_DEVICE value, and the
// FIRST listed entry is what
// a default run presents on — the decoder shares that device, so
// on a hybrid box this line is usually the answer.
let kind = if a.discrete { "discrete" } else { "integrated" };
// `a.index`, NOT the loop position. This list is sorted
// discrete-first for reading, but PUNKTFUNK_VK_DEVICE indexes the
// raw enumeration, which puts the iGPU first on some hybrids —
// printing the loop position would name the other GPU on exactly
// the machines this flag is for. The `i == 0` marker is still the
// loop position, because sorted-first IS what pick_device lands on
// when nothing overrides it.
println!(
"[{i}] {} ({kind}){}",
"[{}] {} ({kind}){}",
a.index,
a.name,
if i == 0 { " <- default presenter" } else { "" }
);
@@ -620,6 +629,26 @@ mod session_main {
println!(" extensions: {}", a.codec_exts.join(", "));
}
}
if adapters.len() > 1 {
// The single most common misreading of this output: seeing a
// capable GPU listed and concluding the decoder will use it.
// Vulkan Video decodes on the PRESENTER's device, and the decoder
// preference does not move the presenter.
println!();
println!(
"Vulkan Video decodes on the presenter's device. PUNKTFUNK_DECODER \
picks the rung,"
);
println!(
"not the GPU — move the presenter with PUNKTFUNK_VK_DEVICE=<index \
above> or"
);
println!(
"PUNKTFUNK_VK_ADAPTER=<name substring>, which is the safer knob \
where two"
);
println!("adapters share a name.");
}
0
}
Err(e) => {
+17 -2
View File
@@ -55,7 +55,19 @@ pub(crate) fn video_decode_gate(
/// hardware decode starts here: WHICH adapter, and does it advertise the codec.
#[derive(Debug, Clone)]
pub struct AdapterDecode {
/// Marketing name — also the `PUNKTFUNK_VK_ADAPTER` match key.
/// The device's position in the RAW `vkEnumeratePhysicalDevices` order — and
/// therefore the value `PUNKTFUNK_VK_DEVICE` takes, because `pick_device` indexes the
/// unsorted list (`devices.get(i)`) before any ranking runs.
///
/// ⚠ NOT the display position. This list is sorted discrete-first for readability,
/// while enumeration order puts the iGPU first on some hybrids — so the two disagree
/// on exactly the machines this probe exists to diagnose. Printing the display
/// position as if it were the env value would hand a hybrid-laptop reporter the
/// number for the other GPU.
pub index: usize,
/// Marketing name — also the `PUNKTFUNK_VK_ADAPTER` match key. Not necessarily
/// unique: a hybrid can expose the same iGPU twice, and a name match then resolves to
/// whichever enumerates first.
pub name: String,
/// Discrete GPUs sort first, exactly as `pick_device` ranks them, so index 0 here is
/// the device a default run will pick.
@@ -743,7 +755,9 @@ pub fn probe_decode() -> Result<Vec<AdapterDecode>> {
// filling locals returned by value.
let devices = unsafe { instance.enumerate_physical_devices() }?;
let mut out: Vec<(u8, AdapterDecode)> = Vec::with_capacity(devices.len());
for pdev in devices {
// `enumerate()` BEFORE any filtering or sorting: this index is what
// `PUNKTFUNK_VK_DEVICE` selects, so it has to survive both.
for (raw_index, pdev) in devices.into_iter().enumerate() {
// SAFETY: per the Vulkan contract above - a read-only query on the live
// instance/device, filling locals returned by value.
let props = unsafe { instance.get_physical_device_properties(pdev) };
@@ -834,6 +848,7 @@ pub fn probe_decode() -> Result<Vec<AdapterDecode>> {
out.push((
rank,
AdapterDecode {
index: raw_index,
name,
discrete: rank == 0,
api_1_3,