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>
This commit is contained in:
2026-07-30 12:30:46 +02:00
co-authored by Claude Opus 5
parent 02e9cc4691
commit 553676282a
17 changed files with 48 additions and 44 deletions
@@ -393,7 +393,7 @@ impl WinDsIdentity {
// 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",
}
}
@@ -403,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",
}
}
}
@@ -644,7 +644,7 @@ pub fn deck_spike_hold(index: u8, secs: u64) -> Result<()> {
// 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.
@@ -73,7 +73,7 @@ impl Ds4WinPad {
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 {
@@ -80,7 +80,7 @@ impl DeckWinPad {
// 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",
&[
@@ -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,