From 94e3629905061a6655ac501c6edc66f48437cc7e Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 13 Aug 2026 13:30:15 +0200 Subject: [PATCH 1/2] chore(deps): wasapi 0.23 -> 0.24 (Windows audio), and correct the five comments that still describe its fixed GetDevice bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version currency for the crate behind the host's WASAPI loopback capture and virtual mic, and the Windows client's render/capture path. No behavior change. The 0.23 -> 0.24 API delta is almost entirely additive (device-change notification callbacks, `AudioMeterInformation`, `HardwareSupport`, `DeviceState: Clone + Copy`). The single removal is `AudioClient::get_bufferframecount`, deprecated since 0.17 in favour of `get_buffer_size` — this tree never called it, so no call site moved. 0.24 also fixes upstream the dangling-`PCWSTR` bug this tree routes around in five places: `DeviceEnumerator::get_device` used to build its argument as `PCWSTR::from_raw(HSTRING::from(id).as_ptr())`, dropping the `HSTRING` at the end of that statement so `GetDevice` read freed memory. Those five comments asserted the bug in the PRESENT tense and are now wrong, so they are corrected here rather than left to mislead. The workarounds themselves STAY: `open_wasapi_device` is still the one resolution path whose errors name the endpoint id and whose `IMMDevice` `probe_activation` needs, and `device_by_id` additionally filters to ACTIVE endpoints (`EnumAudioEndpoints(dir, DEVICE_STATE_ACTIVE)`), which the crate's `get_device` does not. Removing them is a behavior change, not currency. ⚠ This does NOT collapse the duplicate windows-rs. wasapi 0.24 still depends on `windows ^0.62` / `windows-core ^0.62` exactly as 0.23 did, so the crates.io `windows 0.62.2` still sits alongside the pinned git copy that `clients/windows` uses. That duplicate costs build time and binary size, not correctness, and the blanket `[patch.crates-io] windows` that would collapse it stays ruled out — the pinned rev uses header-named features while a dozen manifests still use the old `Win32_*` namespace features. --- Cargo.lock | 4 ++-- crates/pf-client-core/Cargo.toml | 2 +- crates/pf-client-core/src/audio_wasapi.rs | 14 ++++++++------ crates/pf-client-core/src/pad_audio.rs | 7 ++++--- crates/punktfunk-host/Cargo.toml | 2 +- .../src/audio/windows/audio_control.rs | 7 ++++--- .../src/audio/windows/pad_endpoint.rs | 16 ++++++++++------ .../src/audio/windows/wasapi_cap.rs | 7 ++++--- 8 files changed, 34 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ba9a7be0..3cde3cb8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5481,9 +5481,9 @@ dependencies = [ [[package]] name = "wasapi" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80c3aa5d6b0e7acc3ea10cb19c334df0c8d825060f14a30d9e3b03385e6e5175" +checksum = "ee144c215d8d31e94c1a599fbaf540ca82a7017e4f1786816b179f334fac751d" dependencies = [ "log", "num-integer", diff --git a/crates/pf-client-core/Cargo.toml b/crates/pf-client-core/Cargo.toml index a2c67214..1548f1ee 100644 --- a/crates/pf-client-core/Cargo.toml +++ b/crates/pf-client-core/Cargo.toml @@ -150,7 +150,7 @@ libloading = "0.8" x11rb = { version = "0.13", default-features = false } [target.'cfg(windows)'.dependencies] -wasapi = "0.23" +wasapi = "0.24" # Native D3D11VA decode (M5 of the native-decode program): the hand-declared DXVA buffer # layouts and the AuPlan → picparams/qmatrix/slice-control conversion that video_d3d11_native # submits. Windows-only because the rung is; the crate itself is cross-platform CPU code so diff --git a/crates/pf-client-core/src/audio_wasapi.rs b/crates/pf-client-core/src/audio_wasapi.rs index 765281c1..5d646ccc 100644 --- a/crates/pf-client-core/src/audio_wasapi.rs +++ b/crates/pf-client-core/src/audio_wasapi.rs @@ -100,12 +100,14 @@ pub fn devices() -> Result<(Vec, Vec)> { /// audio keeps working, like the PipeWire twin's `target.object` behavior. /// Resolve an active endpoint by id WITHOUT `DeviceEnumerator::get_device`. /// -/// That helper builds its argument as `PCWSTR::from_raw(HSTRING::from(id).as_ptr())` — the -/// `HSTRING` is a temporary, dropped at the end of that statement, so `GetDevice` reads freed -/// memory and misses ids that are perfectly valid. Scanning the active collection touches only -/// safe crate APIs, so it cannot regress the same way. (`punktfunk-host` fixes the same bug with -/// raw COM instead; this crate cannot, because it pins a different `windows` revision than -/// `wasapi` does, making the two `IMMDevice` types incompatible.) +/// Through `wasapi 0.23` that helper built its argument as +/// `PCWSTR::from_raw(HSTRING::from(id).as_ptr())` — the `HSTRING` was a temporary, dropped at the +/// end of that statement, so `GetDevice` read freed memory and missed ids that are perfectly valid. +/// `wasapi 0.24` fixed that upstream. Scanning the active collection touches only safe crate APIs, +/// so it cannot regress the same way, and it additionally filters to ACTIVE endpoints — which is +/// why it stays. (`punktfunk-host` routes around the same bug with raw COM instead; this crate +/// cannot, because it pins a different `windows` revision than `wasapi` does, making the two +/// `IMMDevice` types incompatible.) pub(crate) fn device_by_id( enumerator: &DeviceEnumerator, direction: &Direction, diff --git a/crates/pf-client-core/src/pad_audio.rs b/crates/pf-client-core/src/pad_audio.rs index 16394481..d9de834c 100644 --- a/crates/pf-client-core/src/pad_audio.rs +++ b/crates/pf-client-core/src/pad_audio.rs @@ -920,9 +920,10 @@ fn pad_render_thread( let res = (|| -> anyhow::Result<()> { const BLOCK_ALIGN: usize = PAD_CHANNELS * 4; // f32 interleaved let enumerator = wasapi::DeviceEnumerator::new().context("DeviceEnumerator")?; - // Not `get_device`: that helper resolves through a freed string — see - // [`crate::audio::device_by_id`] (audio_wasapi.rs, mounted as `crate::audio` on - // Windows by lib.rs's `#[path]` swap — there is no `audio_wasapi` module name). + // Not `get_device`: that helper resolved through a freed string through wasapi 0.23, and + // this path additionally wants the ACTIVE-only filter — see [`crate::audio::device_by_id`] + // (audio_wasapi.rs, mounted as `crate::audio` on Windows by lib.rs's `#[path]` swap — + // there is no `audio_wasapi` module name). let device = crate::audio::device_by_id(&enumerator, &Direction::Render, endpoint_id) .map_err(|e| anyhow!("correlated endpoint not found: {e:#}"))?; let mut audio_client = device.get_iaudioclient().context("IAudioClient")?; diff --git a/crates/punktfunk-host/Cargo.toml b/crates/punktfunk-host/Cargo.toml index 7294ce7f..3985b032 100644 --- a/crates/punktfunk-host/Cargo.toml +++ b/crates/punktfunk-host/Cargo.toml @@ -286,7 +286,7 @@ winreg = "0.56" # provider — a small read-only DOM is all we need (Identity/Executable/ShellVisuals/StoreId). roxmltree = "0.21" # WASAPI loopback audio capture (default render endpoint -> 48 kHz stereo f32 for the Opus path). -wasapi = "0.23" +wasapi = "0.24" # Shared host<->driver wire contract for the pf-vdisplay IddCx virtual-display backend: the # control-plane IOCTL codes + `#[repr(C)] Pod` request/reply structs, defined ONCE so host<->driver # ABI drift is a compile error (used from `capture.rs`). The `bytemuck` that serializes those diff --git a/crates/punktfunk-host/src/audio/windows/audio_control.rs b/crates/punktfunk-host/src/audio/windows/audio_control.rs index 1f4f2be2..c05fc116 100644 --- a/crates/punktfunk-host/src/audio/windows/audio_control.rs +++ b/crates/punktfunk-host/src/audio/windows/audio_control.rs @@ -509,9 +509,10 @@ pub(crate) fn restore_default_playback() { /// Open a device by endpoint id, with a name for error context. /// -/// Resolves through [`super::pad_endpoint::open_wasapi_device`], NOT the `wasapi` crate's -/// `DeviceEnumerator::get_device` — that one hands `GetDevice` a freed string (see the helper's -/// docs), so it fails at random on ids that are perfectly valid. +/// Resolves through [`super::pad_endpoint::open_wasapi_device`] rather than the `wasapi` crate's +/// `DeviceEnumerator::get_device`: that one handed `GetDevice` a freed string through 0.23, so it +/// failed at random on ids that are perfectly valid. `wasapi 0.24` fixed that, but we keep the one +/// resolution path — see the helper's docs. pub(crate) fn open_endpoint(ep: &Endpoint) -> Result { super::pad_endpoint::open_wasapi_device(&ep.1) .map_err(|e| anyhow!("open endpoint {:?}: {e:#}", ep.0)) diff --git a/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs b/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs index 4bc65054..383ce98e 100644 --- a/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs +++ b/crates/punktfunk-host/src/audio/windows/pad_endpoint.rs @@ -911,13 +911,17 @@ fn open_mmdevice(endpoint_id: &str) -> Result { /// Open a [`wasapi::Device`] for an endpoint id WITHOUT the crate's `DeviceEnumerator::get_device`. /// -/// `wasapi 0.23` builds that call's argument as -/// `PCWSTR::from_raw(HSTRING::from(device_id).as_ptr())`. The `HSTRING` is a temporary, so it is -/// dropped at the end of THAT statement and `IMMDeviceEnumerator::GetDevice` reads freed memory on -/// the next line. Whether the endpoint is found then depends on what the allocator happened to +/// Through `wasapi 0.23` that call built its argument as +/// `PCWSTR::from_raw(HSTRING::from(device_id).as_ptr())`. The `HSTRING` was a temporary, so it was +/// dropped at the end of THAT statement and `IMMDeviceEnumerator::GetDevice` read freed memory on +/// the next line. Whether the endpoint was found then depended on what the allocator happened to /// leave behind — a heisenbug whose failure mode is `0x80070002` (ERROR_FILE_NOT_FOUND) for an id -/// that is perfectly valid. [`open_mmdevice`] keeps its wide buffer alive across the call, so -/// resolve there and only borrow the crate's wrapper around the resulting interface. +/// that is perfectly valid. **`wasapi 0.24` fixed this upstream** (the `HSTRING` is now bound to a +/// local that outlives the call), so this helper is no longer load-bearing for correctness. +/// +/// We still resolve here, because the `IMMDevice` is wanted in its own right: [`probe_activation`] +/// and the property-store readers below need the raw interface, so routing every lookup through +/// [`open_mmdevice`] keeps ONE resolution path whose errors name the endpoint id. pub(crate) fn open_wasapi_device(endpoint_id: &str) -> Result { let dev = open_mmdevice(endpoint_id)?; wasapi::Device::from_immdevice(dev) diff --git a/crates/punktfunk-host/src/audio/windows/wasapi_cap.rs b/crates/punktfunk-host/src/audio/windows/wasapi_cap.rs index 3e29b0c9..58b42db9 100644 --- a/crates/punktfunk-host/src/audio/windows/wasapi_cap.rs +++ b/crates/punktfunk-host/src/audio/windows/wasapi_cap.rs @@ -747,9 +747,10 @@ enum DefaultKind { Unknown, } -/// Resolves through [`super::pad_endpoint::open_wasapi_device`], NOT the `wasapi` crate's -/// `DeviceEnumerator::get_device` — that one hands `GetDevice` a freed string (see the helper's -/// docs), and a spurious miss here silently downgrades a capturable default to `Unknown`. +/// Resolves through [`super::pad_endpoint::open_wasapi_device`] rather than the `wasapi` crate's +/// `DeviceEnumerator::get_device`: that one handed `GetDevice` a freed string through 0.23, and a +/// spurious miss here silently downgrades a capturable default to `Unknown`. `wasapi 0.24` fixed +/// that, but we keep the one resolution path — see the helper's docs. fn judge_default(wiring: &wiring_plan::Wiring, id: &str) -> DefaultKind { let Ok(dev) = super::pad_endpoint::open_wasapi_device(id) else { return DefaultKind::Unknown; From dfde5080cc882099b4388a9296a95aa232b92719 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Thu, 13 Aug 2026 13:35:30 +0200 Subject: [PATCH 2/2] chore(deps): windows-service 0.7 -> 0.8 (host + tray), removing the last windows-sys 0.52 in the tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Version currency for the SCM plumbing behind `punktfunk-host service` (the dispatcher, control handler and ServiceManager install) and the tray's unprivileged QUERY_STATUS probe. No code changed in either crate. The payoff is dependency unification, not the API. `windows-service 0.7` was the ONLY crate in the workspace still pulling `windows-sys 0.52`, so it alone kept a fourth windows-sys major compiling. It resolves to 0.8.1, which moves to `windows-sys 0.61` — a version the tree already builds — and the duplicate disappears: cargo tree -d --target x86_64-pc-windows-msvc | grep '^windows-sys v' before: 0.45.0, 0.52.0, 0.59.0, 0.61.2 after: 0.45.0, 0.59.0, 0.61.2 Note 0.8.0 would NOT have been enough — it lands on windows-sys 0.59. 0.8.1 is the release that reaches 0.61, hence the `"0.8"` caret plus the comment pinning the reasoning to the manifest. The 0.7 -> 0.8 delta is tiny and touches nothing this tree calls: `ServiceAccess` gains READ_CONTROL / WRITE_DAC / WRITE_OWNER (additive), and `Service::raw_handle` changes return type from `Security::SC_HANDLE` to `Services::SC_HANDLE` as a consequence of the windows-sys bump — we never call it. `ScHandle` is crate-private upstream. No enum gained variants, and the service control handler's match already ends in a `_ =>` arm, so the `#[non_exhaustive]` types stay safe. What remains duplicated (deliberately out of scope here): windows-sys 0.45 via `jni`, and 0.59 via `punktfunk-core` + `if-addrs`. --- Cargo.lock | 6 +++--- crates/punktfunk-host/Cargo.toml | 5 ++++- crates/punktfunk-tray/Cargo.toml | 5 +++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3cde3cb8..31282ab9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5947,13 +5947,13 @@ dependencies = [ [[package]] name = "windows-service" -version = "0.7.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24d6bcc7f734a4091ecf8d7a64c5f7d7066f45585c1861eba06449909609c8a" +checksum = "857224b3b211c6f3616921f081ee54721ee3ad2ace2fac6a6337e032f7b4dcf2" dependencies = [ "bitflags 2.13.1", "widestring", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/crates/punktfunk-host/Cargo.toml b/crates/punktfunk-host/Cargo.toml index 3985b032..16d1e687 100644 --- a/crates/punktfunk-host/Cargo.toml +++ b/crates/punktfunk-host/Cargo.toml @@ -278,7 +278,10 @@ windows = { version = "0.62", features = [ # The SCM plumbing for the `service` subcommand (define_windows_service! / dispatcher / control # handler / ServiceManager install). Wraps the Win32 service API; the supervision loop itself uses # the `windows` crate above. -windows-service = "0.7" +# ⭐ Keep this at 0.8+: 0.7 was the LAST crate in the tree pulling `windows-sys 0.52`, so it alone +# kept a fourth windows-sys major compiling. 0.8.1 moves to `windows-sys 0.61`, which the tree +# already builds, and that duplicate is gone. (0.8.0 is NOT enough — it lands on 0.59.) +windows-service = "0.8" # Read the GOG.com install registry (HKLM\SOFTWARE\WOW6432Node\GOG.com\Games) for the GOG store # provider — ergonomic + correct-by-construction vs. hand-rolled Reg* FFI for subkey enumeration. winreg = "0.56" diff --git a/crates/punktfunk-tray/Cargo.toml b/crates/punktfunk-tray/Cargo.toml index 14e33a4b..3244ef53 100644 --- a/crates/punktfunk-tray/Cargo.toml +++ b/crates/punktfunk-tray/Cargo.toml @@ -38,8 +38,9 @@ rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs", punktfunk-core = { path = "../punktfunk-core", default-features = false, features = ["tls", "ureq-tls"] } [target.'cfg(windows)'.dependencies] -# SCM QUERY_STATUS works unprivileged — the service-state probe. Same crate the host service uses. -windows-service = "0.7" +# SCM QUERY_STATUS works unprivileged — the service-state probe. Same crate the host service uses, +# and it must stay in lockstep with it — see the windows-sys note on the host's declaration. +windows-service = "0.8" windows = { version = "0.62", features = [ "Win32_Foundation", "Win32_Graphics_Gdi",