feat(client): device-pick plumbing — GPU enumeration + audio endpoint targeting

The Settings GPU pick existed (adapter → PUNKTFUNK_VK_ADAPTER) but no Linux
shell could enumerate anything to pick: the GTK shell deliberately links no
Vulkan. pf_presenter::vk::list_adapters() reads the physical devices'
marketing names (no surface, discrete first, deduped — the name is the whole
match key in pick_device), surfaced as `punktfunk-session --list-adapters`.

Audio gets the same treatment for the new speaker_device/mic_device settings
(PipeWire node.name; empty = default): session main maps them onto
PUNKTFUNK_AUDIO_SINK/SOURCE — a hand-set env still wins, like the adapter —
and the playback/mic streams pass them as `target.object` (raw key: the
keys::TARGET_OBJECT constant is feature-gated on a newer libpipewire than we
require). pf_client_core::audio::devices() is the registry roundtrip the
pickers read, exposed for debugging as `punktfunk-session --list-audio`.
The WASAPI leg (Windows endpoint IDs) is still to come; the fields are
ignored there.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-19 19:32:38 +02:00
parent 8fe90a8a4b
commit 40c2f68231
5 changed files with 228 additions and 33 deletions
+2
View File
@@ -33,6 +33,8 @@ mod reconfig;
mod resources;
mod setup;
pub use setup::list_adapters;
/// One presenter iteration's video input.
pub enum FrameInput<'a> {
/// No new frame — re-composite the retained video image (expose/resize).
+46
View File
@@ -497,6 +497,52 @@ impl Presenter {
}
}
/// The physical devices' marketing names — the shells' GPU-picker source
/// (`punktfunk-session --list-adapters`). No surface and no logical device; discrete
/// GPUs first (mirroring `pick_device`'s tie-break), duplicates collapsed (the name is
/// the whole `PUNKTFUNK_VK_ADAPTER` match key, so a second identical card adds nothing).
/// Same 1.3 instance the presenter creates, so the list matches what streaming sees.
pub fn list_adapters() -> Result<Vec<String>> {
let entry = unsafe { ash::Entry::load() }.context("libvulkan not loadable")?;
let app_name = CString::new("punktfunk-session").unwrap();
let app_info = vk::ApplicationInfo::default()
.application_name(&app_name)
.api_version(vk::API_VERSION_1_3);
let instance = unsafe {
entry.create_instance(
&vk::InstanceCreateInfo::default().application_info(&app_info),
None,
)
}
.context("vkCreateInstance")?;
let mut ranked: Vec<(u8, String)> = unsafe { instance.enumerate_physical_devices() }?
.into_iter()
.map(|d| {
let props = unsafe { instance.get_physical_device_properties(d) };
let rank = match props.device_type {
vk::PhysicalDeviceType::DISCRETE_GPU => 0u8,
vk::PhysicalDeviceType::INTEGRATED_GPU => 1,
_ => 2,
};
let name = props
.device_name_as_c_str()
.map(|c| c.to_string_lossy().into_owned())
.unwrap_or_default();
(rank, name)
})
.filter(|(_, n)| !n.is_empty())
.collect();
unsafe { instance.destroy_instance(None) };
ranked.sort_by_key(|(r, _)| *r); // stable: enumeration order within each tier
let mut names: Vec<String> = Vec::new();
for (_, n) in ranked {
if !names.contains(&n) {
names.push(n);
}
}
Ok(names)
}
/// First physical device with a queue family that does graphics + present here;
/// `PUNKTFUNK_VK_DEVICE=<index>` overrides on multi-GPU boxes.
fn pick_device(