Compare commits

...
3 Commits
Author SHA1 Message Date
enricobuehlerandClaude Opus 5 384a0adc83 chore(release): bump workspace version to 0.22.2
android-screenshots / screenshots (push) Successful in 1m32s
audit / cargo-audit (push) Successful in 38s
audit / bun-audit (plugin-kit) (push) Successful in 20s
audit / bun-audit (sdk) (push) Successful in 15s
audit / bun-audit (web) (push) Successful in 15s
audit / docs-site-audit (push) Successful in 14s
audit / pnpm-audit (push) Successful in 16s
android / android (push) Successful in 5m17s
apple / swift (push) Successful in 5m7s
audit / license-gate (push) Successful in 4m33s
decky / build-publish (push) Successful in 48s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 14s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 12s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 51s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 1m3s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 24s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 20s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 35s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 35s
sbom / sbom (push) Successful in 1m17s
linux-client-screenshots / screenshots (push) Successful in 3m43s
docker / deploy-docs (push) Successful in 19s
docker / builders-arm64cross (push) Successful in 13s
web-screenshots / screenshots (push) Successful in 4m25s
ci / web (push) Successful in 1m6s
ci / rust-arm64 (push) Successful in 3m51s
flatpak / build-publish (push) Successful in 5m18s
ci / docs-site (push) Successful in 1m48s
deb / build-publish (push) Successful in 4m0s
deb / build-publish-client-arm64 (push) Successful in 1m45s
arch / build-publish (push) Successful in 8m9s
ci / rust (push) Successful in 8m20s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m50s
deb / build-publish-host (push) Successful in 5m8s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 4m1s
windows-host / package (push) Successful in 11m9s
windows-host / winget-source (push) Skipped
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m41s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 3m1s
release / apple (push) Successful in 26m25s
apple / screenshots (push) Successful in 20m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m21s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m39s
Patch release: 0.22.0 and 0.22.1 gave every default-configured Windows host a
controller no game could see. The pf-dualsense -> pf-gamepad package rename also
renamed a HARDWARE id, so PnP matched none of our models, fell through to the
devnode's synthesized USB ids and let Microsoft's inbox input.inf win — HidUsb
cannot start on a software-enumerated devnode, and without a start there is no
device interface to answer a channel proof. This cut carries that one-line
restore, the [Models]-vs-host guard test, and the Punktfunk display-name rebrand
across the Windows devices and firewall rules.

Windows hosts only; clients and Linux hosts are untouched.

Versions-only lock diff, hand-applied: the 30 workspace-member entries move
0.22.1 -> 0.22.2. Two sets of third-party crates deliberately share our version
space and are untouched — `base64` at 0.22.1, and the eight gtk-rs crates at
0.22.0 (cairo/gdk-pixbuf/gio/graphene/pango); a blanket sed would corrupt both,
so the bump matches on member name only. `cargo metadata --locked` exits 0 on
the pinned toolchain. Release notes in docs/releases/v0.22.2.md seed the release
body per the Model-1 flow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 12:36:11 +02:00
enricobuehlerandClaude Opus 5 553676282a feat(windows): the brand reads Punktfunk on every device Windows shows
deb / build-publish-client-arm64 (push) Failing after 4s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 13s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 11s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 11s
ci / web (push) Successful in 1m53s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 13s
ci / docs-site (push) Successful in 2m3s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 15s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 30s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m24s
windows-drivers / driver-build (push) Successful in 2m30s
docker / builders-arm64cross (push) Successful in 6s
docker / deploy-docs (push) Successful in 29s
android / android (push) Successful in 5m7s
apple / swift (push) Canceled after 4m50s
apple / screenshots (push) Canceled after 0s
deb / build-publish (push) Successful in 4m42s
arch / build-publish (push) Canceled after 5m25s
ci / rust (push) Canceled after 5m26s
ci / rust-arm64 (push) Canceled after 5m31s
deb / build-publish-host (push) Canceled after 5m19s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Canceled after 3m30s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 3m26s
windows-host / package (push) Canceled after 2m29s
windows-host / winget-source (push) Canceled after 0s
windows-drivers / probe-and-proto (push) Successful in 37s
Device Manager, the firewall list and the monitor name all said "punktfunk". The brand
is Punktfunk.

Renamed: the [Strings] blocks of all four driver INFs (device descriptions, install
disks, provider, manufacturer), the `description` on every SwDeviceProfile the host
creates, pf-mouse's HID manufacturer + product strings, pf-vdisplay's IddCx endpoint
friendly + manufacturer names, the EDID 0xFC display-name descriptor — so Windows now
shows `Generic Monitor (Punktfunk)` — and the netsh firewall rule names.

The EDID edit is a single byte (0x70 -> 0x50) and needs no hand-patched checksum:
Edid::generate_with already recomputes both block checksums after patching the serial.

Deliberately left lowercase, because these are IDENTITIES rather than display names and
renaming them would orphan installed state:

  * the SwDeviceCreate enumerator `w!("punktfunk")` — it IS the SWD\PUNKTFUNK\... path
    every pad instance id is built from
  * pf-paths' `join("punktfunk")` — C:\ProgramData\punktfunk
  * the CN=punktfunk-driver cert subject, which purge_driver_certs and both driver build
    scripts match by string
  * install.rs' `lo.contains("punktfunk virtual display")` probes, whose haystack is
    to_ascii_lowercase()d, so they already match the capitalised name

Nothing is orphaned by the renames that DID happen either: netsh rule names,
Get-NetFirewallRule -DisplayName and PowerShell's -match are all case-insensitive, so
the firewall delete paths and reset-pf-vdisplay.ps1's -AdapterName / -GhostMatch
defaults still reap what every release up to 0.22.1 created.

Cosmetic, with two consequences worth knowing: it takes a driver rebuild + re-sign to
appear at all, and an existing devnode keeps its cached FriendlyName until it is
recreated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 12:30:46 +02:00
enricobuehlerandClaude Opus 5 02e9cc4691 fix(host/windows): a virtual DualSense binds our driver again, not Microsoft's
0.22.0 and 0.22.1 hand every default-configured Windows host a controller no game can
see. `GamepadPref::Auto` resolves to DualSense, so this is the pad almost everyone
gets — which is why it reads as "controllers are broken" rather than as one identity
being broken.

The pf-dualsense -> pf-gamepad PACKAGE rename (560e663a) swore the four hardware ids
were untouched, and then renamed one: `WinDsIdentity::dualsense()` started advertising
`pf_gamepad`, a hardware id no INF of ours declares. pf_gamepad.inx still binds
root\pf_dualsense / pf_dualsense / pf_dualshock4 / pf_dualsenseedge / pf_steamdeck,
deliberately — they are the binding contract with every already-installed system.

PnP therefore matched none of our models and fell through to the USB ids the same
devnode synthesizes for the DualSense identity (USB\VID_054C&PID_0CE6, USB\Class_03),
where Microsoft's inbox input.inf wins on signature. HidUsb then bound a
software-enumerated devnode with no USB port behind it and could not start:
CM_PROB_FAILED_START. No start means hidclass never enumerates the collection PDO, so
there is no device interface, so the devnode cannot answer a channel proof, so the v3
delivery gate correctly refuses to hand over the DATA section. Every layer did its
job; the hardware id was wrong.

Measured on .173 against a clean 0.22.1 install — all four identities served by the
one pf_gamepad.inf package:

  DualSense   pf_gamepad        -> input.inf / HidUsb     FAILED_START
  DualShock4  pf_dualshock4     -> oem74.inf / MsHidUmdf  attached
  Edge        pf_dualsenseedge  -> oem74.inf / MsHidUmdf  attached
  Mouse       pf_mouse          -> oem75.inf / MsHidUmdf  attached

and `devgen /add /hardwareid "root\pf_dualsense"` binds oem74.inf, MsHidUmdf,
CM_PROB_NONE, 'punktfunk Virtual DualSense' — the value restored here.

hwid_matches_inf parses pf_gamepad.inx's [Models] and asserts every hardware id the
host puts on a pad devnode is declared there, with a vacuity assert on the parse so a
shape change fails loudly instead of passing empty. DS4_HWID / DECK_HWID exist so the
test pins the same constants the create paths use. This is the guard the rename needed:
the ids have to outlive any future package rename.

The test is cfg(windows) and has NOT been compiled or run here (no Windows toolchain on
the authoring box) — it needs a Windows leg to go green.

Reported twice on 2026-07-30, a GameSir G8+ and a DualSense, both via Android. The host
log names it exactly: `driver=pf_gamepad ... PnP problem code 10` with
`store=driver package present in the driver store`, i.e. not stale drivers and not a
failed install.

Ship this THROUGH THE INSTALLER. install.rs notes that re-creating a SwDevice with a
known instance id revives the existing devnode with its previously-bound driver and
never re-ranks against the store. Instance ids do not change here, so a box already
holding a PF_PAD_0 phantom bound to input.inf would revive input.inf even with the
right hardware id. `driver install --gamepad` sweeps the phantoms; a bare host-binary
swap does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-30 12:30:46 +02:00
20 changed files with 171 additions and 79 deletions
Generated
+30 -30
View File
@@ -947,7 +947,7 @@ dependencies = [
[[package]]
name = "cursor-probe"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"pf-capture",
@@ -1036,7 +1036,7 @@ dependencies = [
[[package]]
name = "display-disturb"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"windows 0.62.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -2221,7 +2221,7 @@ dependencies = [
[[package]]
name = "latency-probe"
version = "0.22.1"
version = "0.22.2"
[[package]]
name = "lazy_static"
@@ -2326,7 +2326,7 @@ dependencies = [
[[package]]
name = "libvpl-sys"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"bindgen",
"cmake",
@@ -2361,7 +2361,7 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "loss-harness"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"punktfunk-core",
]
@@ -2850,7 +2850,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pf-capture"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"ashpd",
@@ -2871,7 +2871,7 @@ dependencies = [
[[package]]
name = "pf-client-core"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"ash",
@@ -2896,7 +2896,7 @@ dependencies = [
[[package]]
name = "pf-clipboard"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"ashpd",
@@ -2914,7 +2914,7 @@ dependencies = [
[[package]]
name = "pf-console-ui"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"ash",
@@ -2935,7 +2935,7 @@ dependencies = [
[[package]]
name = "pf-encode"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"ash",
@@ -2959,7 +2959,7 @@ dependencies = [
[[package]]
name = "pf-ffvk"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"ash",
"bindgen",
@@ -2968,7 +2968,7 @@ dependencies = [
[[package]]
name = "pf-frame"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"libc",
@@ -2980,7 +2980,7 @@ dependencies = [
[[package]]
name = "pf-gpu"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"pf-host-config",
@@ -2994,11 +2994,11 @@ dependencies = [
[[package]]
name = "pf-host-config"
version = "0.22.1"
version = "0.22.2"
[[package]]
name = "pf-inject"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"ashpd",
@@ -3027,14 +3027,14 @@ dependencies = [
[[package]]
name = "pf-paths"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"tracing",
]
[[package]]
name = "pf-presenter"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"ash",
@@ -3049,7 +3049,7 @@ dependencies = [
[[package]]
name = "pf-vdisplay"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"ashpd",
@@ -3082,7 +3082,7 @@ dependencies = [
[[package]]
name = "pf-win-display"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"pf-paths",
@@ -3094,7 +3094,7 @@ dependencies = [
[[package]]
name = "pf-zerocopy"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"ash",
@@ -3302,7 +3302,7 @@ dependencies = [
[[package]]
name = "punktfunk-cli"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"pf-client-core",
"punktfunk-core",
@@ -3313,7 +3313,7 @@ dependencies = [
[[package]]
name = "punktfunk-client-android"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"android_logger",
"jni",
@@ -3329,7 +3329,7 @@ dependencies = [
[[package]]
name = "punktfunk-client-linux"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"async-channel",
@@ -3346,7 +3346,7 @@ dependencies = [
[[package]]
name = "punktfunk-client-session"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"pf-client-core",
@@ -3361,7 +3361,7 @@ dependencies = [
[[package]]
name = "punktfunk-client-windows"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"async-channel",
"ffmpeg-next",
@@ -3381,7 +3381,7 @@ dependencies = [
[[package]]
name = "punktfunk-core"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"aes-gcm",
"bytes",
@@ -3413,7 +3413,7 @@ dependencies = [
[[package]]
name = "punktfunk-host"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"aes",
"aes-gcm",
@@ -3497,7 +3497,7 @@ dependencies = [
[[package]]
name = "punktfunk-probe"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"mdns-sd",
@@ -3511,7 +3511,7 @@ dependencies = [
[[package]]
name = "punktfunk-tray"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"anyhow",
"ksni",
@@ -3534,7 +3534,7 @@ checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
[[package]]
name = "pyrowave-sys"
version = "0.22.1"
version = "0.22.2"
dependencies = [
"bindgen",
"cmake",
+1 -1
View File
@@ -51,7 +51,7 @@ exclude = [
ndk = { path = "clients/android/native/vendor/ndk" }
[workspace.package]
version = "0.22.1"
version = "0.22.2"
edition = "2021"
rust-version = "1.82"
license = "MIT OR Apache-2.0"
@@ -385,9 +385,15 @@ impl WinDsIdentity {
WinDsIdentity {
devtype: 0,
instance_prefix: "pf_pad",
hwid: "pf_gamepad",
// ⚠️ A HARDWARE ID, not the package name. `pf-dualsense` became `pf-gamepad` in
// 560e663a and this line was renamed with it — but the INF deliberately kept the four
// OLD hardware ids, so `pf_gamepad` matched no INF of ours. PnP then fell through to
// the devnode's synthesized USB ids, bound Microsoft's inbox `input.inf`/`HidUsb`, and
// that cannot start on a software-enumerated devnode: CM_PROB_FAILED_START, no HID
// child, no channel proof, and a pad no game ever saw. `hwid_matches_inf` pins it now.
hwid: "pf_dualsense",
usb_vid_pid: "VID_054C&PID_0CE6",
description: "punktfunk Virtual DualSense",
description: "Punktfunk Virtual DualSense",
}
}
@@ -397,7 +403,7 @@ impl WinDsIdentity {
instance_prefix: "pf_edge",
hwid: "pf_dualsenseedge",
usb_vid_pid: "VID_054C&PID_0DF2",
description: "punktfunk Virtual DualSense Edge",
description: "Punktfunk Virtual DualSense Edge",
}
}
}
@@ -633,12 +639,12 @@ pub fn deck_spike_hold(index: u8, secs: u64) -> Result<()> {
instance: &inst,
container_tag: 0x5046_4453, // "PFDS"
container_index: index,
hwid: "pf_steamdeck",
hwid: super::steam_deck_windows::DECK_HWID,
usb_vid_pid: "VID_28DE&PID_1205",
// The Deck's controller interface — the promotion gate the first spike run hit
// (hidapi parses MI_ from the child hwids; absent = interface 0, Steam wants 2).
usb_mi: Some(2),
description: "punktfunk Virtual Steam Deck (spike)",
description: "Punktfunk Virtual Steam Deck (spike)",
})?;
// The spike drives a real pad channel, so it takes the same devnode-proved delivery a session
// pad does — no special case, and no reason for a bring-up tool to run on the old trust.
@@ -802,6 +808,58 @@ mod drain_tests {
assert_eq!(got, vec![vec![0x02, 99]]);
}
/// Every hardware id the host puts on a pad devnode must be one the shipped INF actually
/// declares — otherwise PnP matches none of our models, falls through to the synthesized USB
/// ids on the same devnode, and binds Microsoft's inbox `input.inf`/`HidUsb`, which cannot
/// start on a software-enumerated devnode. The result is a pad that exists, never starts, and
/// never answers a channel proof; that is exactly what shipped in 0.22.0/0.22.1 for the plain
/// DualSense, because the `pf-dualsense` -> `pf-gamepad` PACKAGE rename also rewrote this
/// HARDWARE id (560e663a). The ids are a binding contract with every installed system, so they
/// must outlive any future package rename — this test is what makes that structural.
#[test]
fn hwid_matches_inf() {
let inx = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../packaging/windows/drivers/pf-gamepad/pf_gamepad.inx"
);
let inf = std::fs::read_to_string(inx).expect("read pf_gamepad.inx");
// The [Models] lines: `%DeviceDesc…%=pfGamepad, <hwid>[, <hwid>…]`.
let declared: Vec<String> = inf
.lines()
.map(str::trim)
.filter(|l| !l.starts_with(';'))
.filter_map(|l| l.split_once("=pfGamepad,"))
.flat_map(|(_, ids)| {
ids.split(',')
.map(|id| id.trim().to_ascii_lowercase())
.collect::<Vec<_>>()
})
.collect();
assert!(
declared.len() >= 4,
"parsed {} hardware ids out of {inx} — the [Models] shape changed and this test went \
vacuous; fix the parse rather than deleting the assert",
declared.len()
);
for hwid in [
WinDsIdentity::dualsense().hwid,
WinDsIdentity::dualsense_edge().hwid,
super::super::dualshock4_windows::DS4_HWID,
super::super::steam_deck_windows::DECK_HWID,
] {
let want = hwid.to_ascii_lowercase();
let rooted = format!("root\\{want}");
assert!(
declared
.iter()
.any(|d| d.as_str() == want || d.as_str() == rooted),
"the host creates pad devnodes with hardware id {hwid:?}, which pf_gamepad.inx \
does not declare (it has {declared:?}) — PnP would bind inbox input.inf/HidUsb \
instead and the pad would never start"
);
}
}
#[test]
fn legacy_driver_still_drains_the_latest_slot() {
let mut buf = section();
@@ -21,6 +21,10 @@ use anyhow::Result;
use punktfunk_core::quic::{HidOutput, RichInput};
use std::time::Duration;
/// The hardware id this pad's devnode carries. Must be one `pf_gamepad.inx` declares — a package
/// rename must never touch it (`dualsense_windows::tests::hwid_matches_inf` enforces that).
pub(super) const DS4_HWID: &str = "pf_dualshock4";
/// A single virtual DualShock 4: the `SwDeviceCreate`'d `pf_ds4_<index>` devnode plus the sealed
/// shared-memory channel. Dropping it removes the devnode and closes both sections.
/// `pub`: the type appears as `type Pad` in the `PadProto` impl (a public trait), like the
@@ -66,10 +70,10 @@ impl Ds4WinPad {
instance: &inst,
container_tag: 0x5046_4453, // "PFDS"
container_index: index,
hwid: "pf_dualshock4",
hwid: DS4_HWID,
usb_vid_pid: "VID_054C&PID_09CC",
usb_mi: None,
description: "punktfunk Virtual DualShock 4",
description: "Punktfunk Virtual DualShock 4",
})?; // Propagate, do NOT swallow — see below.
let (hsw, instance_id) = (Some(hsw), instance_id);
// Swallowing a create failure here (the previous behaviour) latched the pad slot to
@@ -56,7 +56,7 @@ fn create_swdevice(index: u8) -> Result<(HSWDEVICE, Option<String>)> {
.encode_utf16()
.chain(std::iter::once(0))
.collect();
let desc: Vec<u16> = "punktfunk Virtual Xbox 360 (XUSB)"
let desc: Vec<u16> = "Punktfunk Virtual Xbox 360 (XUSB)"
.encode_utf16()
.chain(std::iter::once(0))
.collect();
@@ -66,7 +66,7 @@ impl VirtualMouse {
// a mouse (nothing fingerprints them); reusing the shared profile keeps one code path.
usb_vid_pid: "VID_5046&PID_4D4F",
usb_mi: None,
description: "punktfunk Virtual Mouse",
description: "Punktfunk Virtual Mouse",
}) {
Ok((h, i)) => (Some(h), i),
Err(e) => {
@@ -386,7 +386,7 @@ pub fn channel_proof_probe() -> Result<()> {
hwid: "pf_mouse",
usb_vid_pid: "VID_5046&PID_4D4F",
usb_mi: None,
description: "punktfunk Virtual Mouse (channel-proof probe)",
description: "Punktfunk Virtual Mouse (channel-proof probe)",
})?;
let _sw = super::gamepad_raii::SwDevice::new(hsw);
let Some(instance_id) = instance_id else {
@@ -30,6 +30,10 @@ use anyhow::Result;
use punktfunk_core::quic::RichInput;
use std::time::Duration;
/// The hardware id this pad's devnode carries. Must be one `pf_gamepad.inx` declares — a package
/// rename must never touch it (`dualsense_windows::tests::hwid_matches_inf` enforces that).
pub(super) const DECK_HWID: &str = "pf_steamdeck";
/// A single virtual Steam Deck: the `SwDeviceCreate`'d `pf_deck_<index>` devnode plus the sealed
/// shared-memory channel. Dropping it removes the devnode and closes both sections.
/// `pub`: the type appears as `type Pad` in the `PadProto` impl (a public trait).
@@ -70,13 +74,13 @@ impl DeckWinPad {
instance: &inst,
container_tag: 0x5046_4453, // "PFDS"
container_index: index,
hwid: "pf_steamdeck",
hwid: DECK_HWID,
usb_vid_pid: "VID_28DE&PID_1205",
// The wired Deck controller interface — WITHOUT this the HID child carries no MI_
// token, hidapi reports interface 0, and Steam never claims the pad (the N4
// spike's run-1 failure).
usb_mi: Some(2),
description: "punktfunk Virtual Steam Deck",
description: "Punktfunk Virtual Steam Deck",
})?; // Propagate — swallowing latched the slot to a pad with no devnode (see the DS4 twin).
let (hsw, instance_id) = (Some(hsw), instance_id);
// The DATA section goes to whoever THIS devnode says is serving it — not to whatever pid
@@ -92,7 +92,7 @@ unsafe fn ioctl(h: HANDLE, code: u32, input: &[u8], output: &mut [u8]) -> Result
}
/// Reap the ghost (NOT-present) "punktfunk" virtual-monitor device nodes that `IddCxMonitorDeparture`
/// leaves behind. Each departed monitor leaves a not-present "Generic Monitor (punktfunk)" PDO that keeps
/// leaves behind. Each departed monitor leaves a not-present "Generic Monitor (Punktfunk)" PDO that keeps
/// pinning an OS VidPN target against the IddCx adapter's fixed monitor-slot budget; once ~16 accumulate,
/// `IOCTL_ADD` wedges at 0x80070490 (`ERROR_NOT_FOUND`) and every session black-screens until a manual
/// reset/reboot. Removing the not-present PDOs frees the slots — the in-process equivalent of
@@ -532,7 +532,7 @@ impl VdisplayDriver for PfVdisplayDriver {
tracing::warn!("pf-vdisplay IOCTL_CLEAR_ALL failed on startup (continuing)");
}
// CLEAR_ALL only departs the driver's own (in-process) monitor list; it can NOT remove the
// OS-side not-present "Generic Monitor (punktfunk)" PDOs that a previous host-run's monitor
// OS-side not-present "Generic Monitor (Punktfunk)" PDOs that a previous host-run's monitor
// departures left behind. Reap those here so a fresh host start begins with a clean IddCx
// monitor-slot budget — prevents the 0x80070490 slot-exhaustion wedge from carrying across
// restarts (the reason a restart's CLEAR_ALL alone never recovered it before).
+2 -2
View File
@@ -471,7 +471,7 @@ fn web_setup(args: &[String]) -> Result<()> {
"firewall",
"delete",
"rule",
"name=punktfunk web console (TCP 47992)",
"name=Punktfunk web console (TCP 47992)",
],
);
if !run_quiet(
@@ -481,7 +481,7 @@ fn web_setup(args: &[String]) -> Result<()> {
"firewall",
"add",
"rule",
"name=punktfunk web console (TCP 47992)",
"name=Punktfunk web console (TCP 47992)",
"dir=in",
"action=allow",
"protocol=TCP",
+4 -2
View File
@@ -995,7 +995,7 @@ fn add_firewall_rules(allow_public: bool) {
("UDP", "UDP", "47998-48010,9777,5353"),
];
for (suffix, proto, ports) in rules {
let name = format!("punktfunk {suffix}");
let name = format!("Punktfunk {suffix}");
let ok = run_quiet(
"netsh",
&[
@@ -1028,7 +1028,9 @@ fn add_firewall_rules(allow_public: bool) {
fn remove_firewall_rules() {
for suffix in ["TCP", "UDP"] {
let name = format!("punktfunk {suffix}");
// Capital P is the brand; netsh matches a rule name case-INSENSITIVELY, so this still
// reaps the lowercase rules every release up to 0.22.1 created — no orphans on upgrade.
let name = format!("Punktfunk {suffix}");
let _ = run_quiet(
"netsh",
&[
+22
View File
@@ -0,0 +1,22 @@
Wire-compatible with 0.21.x and 0.22.x — nothing about streaming changed, and everything already paired keeps working. This release only touches Windows PCs you stream *to*; the apps on your phone, tablet, Mac and TV are unchanged, as are Linux hosts.
**If you stream to a Windows PC and use a controller, update that PC.** On 0.22.0 and 0.22.1 your controller worked everywhere in the app but no game on the PC ever saw it. Update using the Windows installer rather than replacing the program by hand — the repair includes the controller drivers themselves.
## Fixed
- **Controllers work again on Windows.** Your controller paired, the app responded to it, and a DualSense's touchpad could even still move the mouse pointer on the PC — but games saw no controller at all. Windows had been attaching one of its own built-in drivers to Punktfunk's virtual controller instead of Punktfunk's, and that driver cannot run on a controller that isn't physically plugged in, so the controller was created and then never started. Nothing you could change in the app worked around it.
This hit almost everybody, because it hit the controller type Punktfunk emulates by default. If you had gone into settings and explicitly chosen DualShock 4, Xbox 360, DualSense Edge or Steam Deck, yours kept working the whole time — only the default was broken. Both 0.22.0 and 0.22.1 are affected; 0.21.0 and earlier are not.
## Improved
- **Punktfunk is spelled Punktfunk on Windows.** The virtual display, controllers, mouse and firewall entries Punktfunk creates all announced themselves in lower case. They now carry the proper name — in Device Manager, in your monitor list, and in Windows Firewall. Purely cosmetic, and it appears once the updated drivers install.
## Under the hood (for developers)
- The controller regression was a one-line hardware-id slip. `560e663a` renamed the driver *package* `pf-dualsense``pf-gamepad` and its message asserted the four hardware ids were untouched — but `WinDsIdentity::dualsense()` was renamed along with it, so the host began advertising `pf_gamepad`, which `pf_gamepad.inx` does not declare (it still binds `root\pf_dualsense` / `pf_dualsense` / `pf_dualshock4` / `pf_dualsenseedge` / `pf_steamdeck` — deliberately, as the contract with already-installed systems). PnP matched none of our models, fell through to the USB ids the same devnode synthesizes for the DualSense identity (`USB\VID_054C&PID_0CE6`, `USB\Class_03`), and Microsoft's inbox `input.inf` won on signature. `HidUsb` then bound a software-enumerated devnode with no USB port behind it and could not start — `CM_PROB_FAILED_START`. Without a start, hidclass never enumerates the collection PDO, so there is no device interface, so the devnode cannot answer a channel proof, and the gamepad channel's v3 delivery gate correctly refused to hand over the shared input section. Every layer downstream behaved exactly as designed.
- Measured against a clean install, all four identities served by the one `pf_gamepad.inf`: DualSense (`pf_gamepad`) → `input.inf`/`HidUsb`, failed start; DualShock 4 (`pf_dualshock4`), Edge (`pf_dualsenseedge`) and the virtual mouse (`pf_mouse`) → our package, attached. `devgen /add /hardwareid "root\pf_dualsense"` binds our INF with no problem code.
- A new `hwid_matches_inf` test parses `pf_gamepad.inx`'s `[Models]` section and asserts every hardware id the host puts on a pad devnode is declared there, with a vacuity check on the parse so a shape change fails loudly rather than passing empty. `DS4_HWID` / `DECK_HWID` exist so the test pins the same constants the create paths use.
- Why the installer and not a binary swap: re-creating a software device with a known instance id revives the existing devnode with its previously-bound driver and never re-ranks against the driver store. Instance ids did not change, so a PC still holding a stale virtual pad bound to `input.inf` would revive `input.inf` even with the correct hardware id. `driver install --gamepad` sweeps those devnodes; replacing the host executable alone does not.
- The rename covers the four driver INFs' `[Strings]`, every `SwDeviceProfile` description, `pf-mouse`'s HID manufacturer and product strings, pf-vdisplay's IddCx endpoint names, the EDID `0xFC` display-name descriptor (one byte — `Edid::generate_with` recomputes both block checksums), and the netsh rule names. Left in lower case on purpose, because they are identities rather than display names: the `SwDeviceCreate` enumerator (it *is* the `SWD\PUNKTFUNK\…` instance-id path), `%ProgramData%\punktfunk`, the `CN=punktfunk-driver` cert subject, and `install.rs`' already-lowercased device probes. netsh, `Get-NetFirewallRule -DisplayName` and PowerShell's `-match` are case-insensitive, so the delete paths still reap what earlier releases created.
- No wire, ABI or driver-protocol changes: wire protocol 2, C ABI 13, Windows virtual-gamepad channel 3, virtual-display driver protocol 6 — identical to 0.22.0 and 0.22.1.
@@ -90,13 +90,13 @@ ServiceBinary="%13%\pf_gamepad.dll"
pf_gamepad.dll
[Strings]
ProviderString ="punktfunk"
ManufacturerString ="punktfunk"
ProviderString ="Punktfunk"
ManufacturerString ="Punktfunk"
ClassName ="HID device"
Disk_Description ="punktfunk Gamepad Installation Disk"
Disk_Description ="Punktfunk Gamepad Installation Disk"
; One per hardware id — these are what Device Manager shows. Keep them aligned with the product
; strings the driver serves per device_type (src/lib.rs `on_get_string`).
DeviceDesc ="punktfunk Virtual DualSense"
DeviceDescDS4 ="punktfunk Virtual DualShock 4"
DeviceDescEdge ="punktfunk Virtual DualSense Edge"
DeviceDescDeck ="punktfunk Virtual Steam Deck Controller"
DeviceDesc ="Punktfunk Virtual DualSense"
DeviceDescDS4 ="Punktfunk Virtual DualShock 4"
DeviceDescEdge ="Punktfunk Virtual DualSense Edge"
DeviceDescDeck ="Punktfunk Virtual Steam Deck Controller"
@@ -72,8 +72,8 @@ ServiceBinary="%13%\pf_mouse.dll"
pf_mouse.dll
[Strings]
ProviderString ="punktfunk"
ManufacturerString ="punktfunk"
ProviderString ="Punktfunk"
ManufacturerString ="Punktfunk"
ClassName ="HID device"
Disk_Description ="punktfunk Mouse Installation Disk"
DeviceDesc ="punktfunk Virtual Mouse"
Disk_Description ="Punktfunk Mouse Installation Disk"
DeviceDesc ="Punktfunk Virtual Mouse"
@@ -428,14 +428,14 @@ fn on_get_string(request: &Request) -> NTSTATUS {
};
let string_id = id_val & 0xFFFF;
let s: String = match string_id {
0 | 0x000E => "punktfunk".into(),
0 | 0x000E => "Punktfunk".into(),
// (2) The SERIAL carries the channel proof — the one transport measured to reach a UMDF HID
// minidriver from user mode (`HidD_GetSerialNumberString`, zero-access handle, verified on
// .173). Safe HERE and only here: nothing reads the virtual mouse's serial, whereas the pads'
// serials are what SDL and Steam dedup controllers on. The old value was the inert
// "PFMOUSE00"; the proof text is just as inert and does the security work.
2 | 0x0010 => ChannelProof::new(CHANNEL.index(), std::process::id()).to_hid_string(),
_ => "punktfunk Virtual Mouse".into(),
_ => "Punktfunk Virtual Mouse".into(),
};
let mut wide: Vec<u8> = Vec::with_capacity(s.len() * 2 + 2);
for u in s.encode_utf16() {
@@ -75,10 +75,10 @@ UMDriverCopy=12,UMDF
pf_vdisplay.dll
[Strings]
ManufacturerName="punktfunk"
DiskName="punktfunk Virtual Display Installation Disk"
ManufacturerName="Punktfunk"
DiskName="Punktfunk Virtual Display Installation Disk"
WudfRdDisplayName="Windows Driver Foundation - User-mode Driver Framework Reflector"
DeviceName="punktfunk Virtual Display"
DeviceName="Punktfunk Virtual Display"
REG_MULTI_SZ=0x00010000
REG_SZ=0x00000000
@@ -82,8 +82,8 @@ pub fn init_adapter(device: WDFDEVICE) -> NTSTATUS {
diag.Size = core::mem::size_of::<iddcx::IDDCX_ENDPOINT_DIAGNOSTIC_INFO>() as u32;
diag.GammaSupport = iddcx::IDDCX_FEATURE_IMPLEMENTATION::IDDCX_FEATURE_IMPLEMENTATION_NONE;
diag.TransmissionType = iddcx::IDDCX_TRANSMISSION_TYPE::IDDCX_TRANSMISSION_TYPE_WIRED_OTHER;
diag.pEndPointFriendlyName = wstr!("punktfunk Virtual Display Adapter");
diag.pEndPointManufacturerName = wstr!("punktfunk");
diag.pEndPointFriendlyName = wstr!("Punktfunk Virtual Display Adapter");
diag.pEndPointManufacturerName = wstr!("Punktfunk");
diag.pEndPointModelName = wstr!("Virtual Display");
// SAFETY: `version` is a stack local that outlives this `init_adapter` call; IddCxAdapterInitAsync
// (below) reads through these pointers SYNCHRONOUSLY, before `version` drops — the pointer never escapes.
@@ -6,7 +6,9 @@
//! made "Use HDR" never appear for the virtual display). The base block declares EDID 1.4 + 10-bit
//! digital so the panel's bit depth is unambiguous.
//!
//! Identity: manufacturer "PNK" (bytes 8-9), product name "punktfunk" (the 0xFC display descriptor). The
//! Identity: manufacturer "PNK" (bytes 8-9), product name "Punktfunk" (the 0xFC display descriptor
//! this is what Windows shows as `Generic Monitor (Punktfunk)`; byte 127's checksum is recomputed in
//! [`Edid::generate_with`], so editing the name here needs no hand-patched checksum). The
//! serial-number field (base offset 0x0C, little-endian) encodes the per-monitor index so
//! `parse_monitor_description` can map an EDID the OS hands back to its monitor; [`Edid::generate_with`]
//! patches that serial and recomputes BOTH block checksums (base byte 127 + extension byte 255). The
@@ -34,7 +36,7 @@ const BASE: [u8; 128] = [
0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E,
0x00, 0x00, 0x00, 0xFD, 0x00, 0x17, 0xF0, 0x0F, // display range-limits descriptor
0xFF, 0x0F, 0x00, 0x0A, 0x20, 0x20, 0x20, 0x20,
0x20, 0x20, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x70, // name descriptor "punktfunk"
0x20, 0x20, 0x00, 0x00, 0x00, 0xFC, 0x00, 0x50, // name descriptor "Punktfunk"
0x75, 0x6E, 0x6B, 0x74, 0x66, 0x75, 0x6E, 0x6B,
0x0A, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, // empty 4th descriptor...
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@@ -58,7 +58,7 @@ UmdfLibraryVersion=$UMDFVERSION$
ServiceBinary=%13%\pf_xusb.dll
[Strings]
ProviderString = "punktfunk"
ProviderString = "Punktfunk"
StdMfg = "(Standard system devices)"
DiskId1 = "punktfunk XUSB Installation Disk"
DeviceDesc = "punktfunk Virtual Xbox 360 (XUSB)"
DiskId1 = "Punktfunk XUSB Installation Disk"
DeviceDesc = "Punktfunk Virtual Xbox 360 (XUSB)"
+1 -1
View File
@@ -351,7 +351,7 @@ Filename: "{app}\punktfunk-host.exe"; Parameters: "driver uninstall --gamepad";
; Stop + remove the PunktfunkWeb task and its firewall rule (leaves %ProgramData%\punktfunk config,
; like the host uninstall does).
Filename: "powershell.exe"; \
Parameters: "-NoProfile -ExecutionPolicy Bypass -Command ""Stop-ScheduledTask -TaskName PunktfunkWeb -ErrorAction SilentlyContinue; Get-NetTCPConnection -LocalPort 47992,3000 -State Listen -ErrorAction SilentlyContinue | ForEach-Object {{ Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }; Unregister-ScheduledTask -TaskName PunktfunkWeb -Confirm:$false -ErrorAction SilentlyContinue; Get-NetFirewallRule -DisplayName 'punktfunk web console (*' -ErrorAction SilentlyContinue | Remove-NetFirewallRule"""; \
Parameters: "-NoProfile -ExecutionPolicy Bypass -Command ""Stop-ScheduledTask -TaskName PunktfunkWeb -ErrorAction SilentlyContinue; Get-NetTCPConnection -LocalPort 47992,3000 -State Listen -ErrorAction SilentlyContinue | ForEach-Object {{ Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }; Unregister-ScheduledTask -TaskName PunktfunkWeb -Confirm:$false -ErrorAction SilentlyContinue; Get-NetFirewallRule -DisplayName 'Punktfunk web console (*' -ErrorAction SilentlyContinue | Remove-NetFirewallRule"""; \
Flags: runhidden waituntilterminated; RunOnceId: "PunktfunkWebCleanup"
#endif
#ifdef WithScripting
+6 -6
View File
@@ -7,7 +7,7 @@
.DESCRIPTION
Sustained connect/disconnect churn (e.g. a client reconnect loop x the host's 8 pipeline-build
retries - ~100 ADD/REMOVE cycles) exhausts the driver's IddCx monitor slots: the per-monitor
target_ids climb, ghost "Generic Monitor (punktfunk)" device nodes pile up, and eventually
target_ids climb, ghost "Generic Monitor (Punktfunk)" device nodes pile up, and eventually
IOCTL_ADD returns 0x80070490 ERROR_NOT_FOUND ("Element nicht gefunden"). Every session then fails
to create a virtual output -> the client gets a hard blackscreen. A host-service restart's
IOCTL_CLEAR_ALL does NOT recover it; the driver instance itself must be reloaded.
@@ -16,16 +16,16 @@
1. Stop the host service (it holds the driver's control device).
2. pnputil /remove-device the GHOST (Status != OK = not-present) punktfunk virtual-monitor nodes
that accumulated - the root of the slot exhaustion.
3. Disable + Enable the pf-vdisplay adapter (ROOT\DISPLAY\*, "punktfunk Virtual Display") to
3. Disable + Enable the pf-vdisplay adapter (ROOT\DISPLAY\*, "Punktfunk Virtual Display") to
reload the IddCx driver instance and reset its monitor list. (Restart-PnpDevice does NOT exist
on this box's PowerShell, so we disable+enable explicitly.)
4. Restart the host service.
Avoids a reboot on purpose (this box boots to Proxmox).
.PARAMETER Service Host service name. Default PunktfunkHost.
.PARAMETER AdapterName FriendlyName substring of the IddCx adapter to cycle. Default "punktfunk
.PARAMETER AdapterName FriendlyName substring of the IddCx adapter to cycle. Default "Punktfunk
Virtual Display" (NOT SudoVDA's "SudoMaker Virtual Display Adapter").
.PARAMETER GhostMatch FriendlyName substring of the virtual monitors to reap. Default "punktfunk".
.PARAMETER GhostMatch FriendlyName substring of the virtual monitors to reap. Default "Punktfunk".
.PARAMETER KeepGhosts Skip the ghost-node cleanup; only cycle the adapter.
.PARAMETER NoHost Don't stop/start the host service (just reset the driver) - used by
redeploy-pf-vdisplay.ps1, which manages the service itself.
@@ -41,8 +41,8 @@
[CmdletBinding()]
param(
[string]$Service = 'PunktfunkHost',
[string]$AdapterName = 'punktfunk Virtual Display',
[string]$GhostMatch = 'punktfunk',
[string]$AdapterName = 'Punktfunk Virtual Display',
[string]$GhostMatch = 'Punktfunk',
[switch]$KeepGhosts,
[switch]$NoHost,
[switch]$Verify,