Commit Graph
135 Commits
Author SHA1 Message Date
enricobuehler 37781a610f fix(installer/windows): only a host that will actually RUN is a conflict
docker / deploy-docs (push) Successful in 41s
ci / web (push) Successful in 56s
ci / docs-site (push) Successful in 1m50s
apple / swift (push) Successful in 5m41s
ci / bench (push) Successful in 7m1s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 2m3s
ci / rust-arm64 (push) Successful in 9m51s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 27s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 27s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 30s
deb / build-publish-host (push) Successful in 11m32s
deb / build-publish-client-arm64 (push) Successful in 10m59s
decky / build-publish (push) Successful in 41s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 30s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6m19s
android / android (push) Successful in 17m31s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Failing after 5m35s
windows-host / package (push) Successful in 11m21s
windows-host / winget-source (push) Skipped
arch / build-publish (push) Successful in 19m9s
docker / build-push-arm64cross (push) Successful in 5m7s
ci / rust (push) Successful in 22m25s
deb / build-publish (push) Successful in 10m49s
apple / screenshots (push) Successful in 23m8s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 21m34s
Field report within hours of 0.20.0 (HitFrostbite): `winget install
unom.PunktfunkHost` fails with "Installer failed with exit code: 1",
0x8A150006, after the hash verifies and the install starts.

That is Setup aborting on purpose. `StreamHostPresent` counted a conflicting
host as present if its SCM service key existed at ANY start type, OR if a bare
Program Files\<Name> directory existed — so a disabled service, or a folder left
behind by an uninstall, read as a live conflict. 3d893013 then made the warning
a SuppressibleMsgBox defaulting to IDNO so unattended installs could not hang on
it; under winget's /VERYSILENT /SUPPRESSMSGBOXES that default aborts
InitializeSetup, and Inno exits "failed to initialize" = code 1. Net effect:
anyone with Sunshine on disk — the exact audience most likely to migrate — could
not install via winget at all, with no visible reason.

The other candidate for exit 1 is excluded rather than assumed: the manifest
declares MinimumOSVersion 10.0.22621.0 matching the script's MinVersion, so
winget refuses an unsupported OS BEFORE downloading, and the report shows the
hash verified and the install starting. The conflict check is also the only
`Result := False` in InitializeSetup.

Detection now keys on the service's `Start` value (0 boot / 1 system /
2 automatic / 3 manual / 4 disabled), counting only 0-2 — a host that comes up
on its own is the only kind that can take the GameStream ports or load a second
virtual-display driver. Directories no longer count at all: they were the
noisiest signal and the one that survives an uninstall. This makes the installer
agree with the tray, which dropped its always-on warning over a merely-INSTALLED
Sunshine in this same release (3e782852) for exactly the same reason. The
message now says "installed and set to start automatically" and offers disabling
the service as a remedy, both of which are what is actually detected.

A conflicting host running as a plain user process is deliberately not detected:
pure Pascal cannot enumerate processes, it is a runtime rather than install-time
condition, and the host already reports it via the `punktfunk::detect` startup
warning, `detect-conflicts`, and /api/v1/local/summary.

Verified by compiling the [Code] section with ISCC 6.7.1 on the windows-amd64
runner (WithWeb defined, as CI does). That caught two defects in my own first
draft that review would not have: a continuation line STARTING with #13#10, which
ISPP reads as a preprocessor directive ("Unknown preprocessor directive", compile
aborted), and `{commonpf}` written inside a `{ }` comment, which closes it early
since Inno comments do not nest. Both are noted at the sites so they are not
reintroduced.
2026-07-27 09:00:34 +02:00
enricobuehlerandClaude Opus 5 3d89301336 fix(installer/windows): a silent install can't hang on a prompt, and an upgrade keeps your choices
Three faults that only bite when nobody is watching the wizard — which is how a
package manager always runs us, and how a re-run by hand half-behaves today.

The conflicting-host warning used a plain MsgBox. That ignores /SUPPRESSMSGBOXES
and displays even under /VERYSILENT, so an unattended install onto a box already
running Sunshine/Apollo would sit on a modal dialog nobody can see or click.
SuppressibleMsgBox with an IDNO default aborts instead, which is the honest
answer for a combination the message itself calls unsupported.

The GameStream and Public-firewall choices were re-applied on every run. A silent
run has no wizard, so every task falls back to its script default and the
installer confidently re-asserted it: a user who enabled GameStream had it turned
off again, and one who opened the firewall on Public networks had it quietly
revoked (that task is default-unchecked, so the reset ran the other way). Both
params are now fresh-install-only, keyed off whether host.env already exists.
`service install` already read an absent --gamestream as "keep host.env as-is";
--allow-public-network becomes tri-state the same way, resolving an absent flag
from the marker the previous install recorded. It is strict on the value: a
typo'd =of must not fall through to a marker that may say true, because that
turns a mistyped opt-OUT into leaving Public open.

Also brands the installer "Punktfunk Host" in the strings a user actually sees —
Add/Remove Programs, the Start Menu group, the wizard task text. Paths are
untouched; renaming those would relocate installs and orphan existing config.

Verified on the windows-amd64 runner: ISCC compiles the script (with WithWeb
defined, so the password page parses too), and cargo check + clippy -D warnings
pass for punktfunk-host with nvenc,amf-qsv,qsv.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 19:09:13 +02:00
enricobuehlerandClaude Opus 5 0d9d78398c fix(drivers/windows): name each virtual pad for what it is — one shared description read as "the setting did nothing"
deb / build-publish (push) Successful in 9m31s
windows-drivers / probe-and-proto (push) Successful in 42s
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m9s
windows-drivers / driver-build (push) Successful in 2m1s
ci / bench (push) Successful in 7m35s
decky / build-publish (push) Successful in 20s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
deb / build-publish-host (push) Successful in 10m4s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
android / android (push) Successful in 12m30s
ci / rust-arm64 (push) Successful in 13m55s
apple / swift (push) Successful in 5m28s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m41s
deb / build-publish-client-arm64 (push) Successful in 11m7s
windows-host / package (push) Successful in 16m55s
arch / build-publish (push) Successful in 20m3s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8m46s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9m7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10m54s
docker / deploy-docs (push) Successful in 34s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 4m4s
flatpak / build-publish (push) Failing after 8m23s
docker / build-push-arm64cross (push) Successful in 3m59s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 3m28s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 4m48s
ci / rust (push) Successful in 32m19s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 5m55s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m9s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m53s
release / apple (push) Successful in 29m51s
apple / screenshots (push) Successful in 24m26s
`pf_dualsense.inx` gave all four hardware ids a single %DeviceDesc%, so Device
Manager labelled an emulated DualShock 4, DualSense Edge and Steam Deck pad
"punktfunk Virtual DualSense". The HID layer was always per-type — device_type
picks the PID (09CC for DS4), the report descriptor and the product string — but
the one place a user goes to check said DualSense for every choice, which reads
exactly like the controller-type setting being ignored.

Split into four model lines over the same install section, one description each.
No binding, service or descriptor change; stampinf's 9.9.MMdd.HHmm DriverVer
increments on every build, so pnputil takes the update. InfVerif on the WDK
runner: INF is VALID.

Also correct the Slot.pref comment from the previous commit: emulating a
DualShock 4 gives up adaptive triggers by construction. HidOutput::Trigger is
emitted only by dualsense_proto, and a DS4 has no trigger-effect reports — the
host never generates any to send. Rumble and the lightbar remain.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 16:56:44 +02:00
enricobuehlerandClaude Fable 5 248e1cbf08 fix(pf-vdisplay/driver): report cursor_excluded adapter-wide — the declare's exclusion is not per-target
ci / web (push) Successful in 51s
ci / docs-site (push) Successful in 1m5s
apple / swift (push) Successful in 1m20s
windows-drivers / probe-and-proto (push) Successful in 1m2s
decky / build-publish (push) Successful in 28s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 21s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 16s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 11s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
windows-drivers / driver-build (push) Successful in 2m23s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m5s
ci / bench (push) Successful in 6m3s
apple / screenshots (push) Successful in 6m41s
deb / build-publish (push) Successful in 12m20s
arch / build-publish (push) Successful in 12m47s
deb / build-publish-host (push) Successful in 13m7s
android / android (push) Successful in 16m14s
windows-host / package (push) Successful in 15m53s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m23s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m8s
ci / rust (push) Successful in 26m6s
docker / deploy-docs (push) Canceled after 25s
Field report (iPad via GameStream, 2026-07-23): cursor missing from the
stream. The session's fresh target 257 reported cursor_excluded=false
(DECLARED_TARGETS is keyed per target and only 259 had declared), so the
host skipped forced-composite — but DWM's pointer exclusion after an
IddCx hardware-cursor declare reaches EVERY later monitor of the adapter,
not just the declaring target (proven on-glass: declare on 259 under
WUDFHost 2192, then 257's frames streamed pointer-free with CURSOR_SHOWING
set and SM_MOUSEPRESENT true). GameStream/Moonlight clients cannot draw a
forwarded cursor, so those sessions were silently cursor-less.

ADD replies now report any-declare-this-WUDFHost-life (the state's real
scope — it dies with the adapter reset, same as before); per-target ids
stay only as the dbglog audit trail. Channel-less sessions on a declared
adapter then hit the existing forced-composite gate (GDI poller + blend)
and carry the pointer in-video. Host code unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:58:54 +02:00
enricobuehlerandClaude Fable 5 7058647264 fix(vdisplay/capture): channel-less sessions composite the pointer on a sticky-declared target
A declared IddCx hardware cursor is IRREVOCABLE for its OS target's life
(§8.6), and the sticky exclusion survives monitor REMOVE→ADD because each
client gets a STABLE target id — so once any desktop-mode session declared,
every later pure-capture session on that target streamed a cursor-less
desktop: DWM excluded the pointer, no channel forwarded it, no blend drew it
(the exact no-regression gap §8.6's per-session cap gate cannot see).

Driver: track every successful SetupHardwareCursor per target
(DECLARED_TARGETS — scoped to the WUDFHost's life, exactly the sticky
state's scope) and report it in a new AddReply::cursor_excluded tail field
(dual-size discipline, both skews degrade cleanly; no proto bump).

Host: the flag rides AddedMonitor → Monitor → WinCaptureTarget; a session
WITHOUT the cursor channel on a flagged target forces composite mode in the
IDD-push capturer — GDI poller + blend for the session's life, pinned on
(set_cursor_forward cannot clear it: with no client drawing, un-compositing
would erase the pointer entirely).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:47:53 +02:00
enricobuehlerandClaude Opus 4.8 89cfef429e fix(windows/vdisplay): cursor-render state sticky per TARGET across monitor generations
On-glass: 're-setup on swap-chain assign -> 0x0' fired AFTER 'enable=0
stored' — duplicate/successor monitor entries (re-arrival churn) kept
their default-true flag and re-declared the hardware cursor right after
the composite flip's re-commit, undoing it.

The desired state now lives in a per-target static
(CURSOR_FORWARD_DESIRED): every arrival inherits it (no generation can
resurrect a declare the session turned off), and the flip stamps EVERY
live entry matching the target, declaring against whichever has the
live worker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Opus 4.8 b50e4767de fix(windows/cursor-flip): flag + forced same-mode re-commit — the working un-declare
The empty-caps un-declare is REJECTED by IddCx (STATUS_INVALID_PARAMETER,
on-glass driver 9.9.0722.1407) — there is no un-declare DDI. The
composite flip now works with the only lever the OS gives us: every
mode COMMIT reverts the path to the software cursor, and the driver
skips its per-commit re-declare while cursor_forward_on is off. So:

- driver: disable stores the flag only (no DDI call); enable still
  declares immediately against the live worker's event.
- host capturer: after a successful disable flip, force a same-mode
  re-commit (win_display::force_mode_reenumeration) — DWM composites
  the pointer from the very next commit; the swap-chain flap is the
  class the driver's preserved-publisher machinery already rides out.
- state now survives re-arrivals end-to-end: the capturer caches the
  APPLIED state (cleared on every channel (re)delivery, which
  re-created driver entries need — their flag defaults to declared),
  and the stream loop re-applies every tick instead of edge-gating
  (steady-state cost: one Option compare).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Opus 4.8 6a8df2ba97 fix(windows/cursor-flip): survive driver-side monitor re-arrival — flag-based flip + channel re-delivery
On-glass (.173, match-window session): every window-size convergence
re-creates the driver-side monitor (re-arrival resize / sibling-session
slot churn), destroying the cursor worker the channel was delivered to —
the flip then died NOT_FOUND (no entry with target_id ∧ worker) and the
declared state was lost entirely.

Driver: set_cursor_forward becomes STATE, not an edge on one monitor
generation — find by target_id ∧ hw_cursor, store cursor_forward_on
even without a live worker (it steers the next delivery/re-setup);
declare/un-declare only when a worker exists. Channel delivery honors
the flag: setup_and_spawn(declare=false) adopts + spawns WITHOUT
declaring (composite mode), so a later enable-flip declares against the
worker's event.

Host: retain the SET_CURSOR_CHANNEL sender; extract
deliver_cursor_channel and RE-deliver the surviving section on every
ring recreate and — with one retry — when a flip IOCTL fails
(the re-arrived entry gets a fresh worker, declared per its flag).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Opus 4.8 030a779391 feat(windows/vdisplay): the driver leg of the mid-stream cursor-render flip
Belongs to 493f4fae (which shipped only the crates/ side — this
packaging/ half was left unstaged, so the deployed driver silently
lacked the IOCTL and every flip died NOT_FOUND at dispatch):
IOCTL_SET_CURSOR_FORWARD dispatch + monitor::set_cursor_forward
(cursor_forward_on flag, resetup gate) + cursor_worker::
unsetup_hardware_cursor (empty-caps un-declare experiment).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Opus 4.8 9162e7b451 fix(windows/vdisplay): drop now-unused INFINITE import (poll-loop wait)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Opus 4.8 d2cc770938 fix(windows/vdisplay): QueryHardwareCursor3 + per-mode-commit cursor re-setup — M2c findings
Durable results of the M2c on-glass bring-up (.173, 2026-07-22):

- The base IddCxMonitorQueryHardwareCursor DDI slot is stubbed to
  STATUS_NOT_SUPPORTED on WDK 26100 — add the ...QueryHardwareCursor3
  wrapper (wdk-iddcx) and drain it instead; v3 X/Y are only meaningful
  when PositionValid, so a position-invalid tick keeps the prior position.
- Hardware-cursor setup is per-mode-commit: the OS silently reverts to a
  software cursor on every mode commit, so re-issue the setup on each
  swap-chain assignment (monitor::resetup_cursor, called from
  assign_swap_chain) or the query fails NOT_SUPPORTED forever.
- One caps definition for initial setup and re-setup, resting at XOR
  FULL: the query delivers ONLY alpha shapes in every configuration
  (all three ColorXorCursorSupport levels, event-driven and 30 Hz
  polled) — masked/monochrome cursors never arrive. FULL keeps the
  frame cursor-free for ALL cursor types; the full-fidelity shape comes
  from the session-side cursor source in the host
  (design/remote-desktop-sweep.md §8).
- Poll the query at ~30 Hz instead of pure event-wait: masked cursors
  never fire the data event, and the seqlock's position/visibility
  should stay fresh regardless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Fable 5 3a34440a6b feat(windows): IddCx hardware-cursor channel — remote-desktop sweep M2c
Brings the cursor channel to Windows hosts. The pf-vdisplay driver
declares an IddCx hardware cursor for sessions that negotiated
cursor-forward — DWM then EXCLUDES the pointer from the IDD frame and
delivers shape/position out-of-band, into the same CursorOverlay →
forwarder → wire → client pipeline the Linux portal path uses.

- pf-driver-proto v5 (additive, host floor stays 3): AddRequest's spare
  tail becomes hw_cursor (same size/offsets); IOCTL_SET_CURSOR_CHANNEL
  delivers a host-created CursorShm section (64-byte seqlock header +
  256² shape buffer, layout pinned + tested). No event crosses the
  boundary — the host polls at encode-tick pace.
- driver: wdk-iddcx grows the two cursor DDI wrappers; a per-monitor
  cursor worker (event wait → QueryHardwareCursor → seqlock publish)
  starts only when BOTH the ADD asked and the channel arrived, so a
  failed delivery leaves DWM compositing as today. Shape bytes ship raw
  (BGRA/masked + pitch); the host converts.
- host: the section rides the existing sealed-channel broker (least-
  privilege dup, remote reap on failure); IddPushCapturer::cursor()
  seqlock-reads → CursorOverlay (BGRA→RGBA, masked-color approximation,
  desktop→frame origin shift, per-shape conversion cache). New
  Capturer::cursor() trait hook — the encode loop prefers it over the
  frame-attached overlay because hardware-cursor moves produce NO new
  frame on a static desktop. hw_cursor survives the re-arrival resize
  (carried on the manager's Monitor).
- negotiation: cursor_forward grows the Windows arm (client cap ∧
  driver proto ≥ 5, probed once via the control device); SessionPlan
  carries cursor_forward → OutputFormat.hw_cursor.
- drive-by: wdk-probe's two pre-existing same-type casts (clippy) and
  pf-vdisplay's stale spike-test refs (crate::win_display moved to
  pf-win-display; tracing-subscriber was never a dep) repaired.

Verified: proto tests on Mac AND MSVC (15/15 incl. the CursorShm
layout pin); clippy -D warnings for proto/frame/capture/vdisplay/host
on Linux (.21) and native Windows (.173); the DRIVER workspace clippy
-D warnings green against the real WDK 10.0.26100 bindgen (DDI names,
enum variants and IDARG layouts all bind). On-box driver deploy +
on-glass validation follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 23:43:23 +02:00
enricobuehlerandClaude Fable 5 84c47cd0a7 feat(security): run the Windows plugin runner as LocalService, not SYSTEM
The PunktfunkScripting scheduled task ran operator-installed plugin code
as SYSTEM with -RunLevel Highest — any plugin defect was a full
compromise of the box. The principal is now NT AUTHORITY\LocalService
(minimal privileges, no password to manage, exists at boot, loopback
networking works), registered without RunLevel:

- installer: New-ScheduledTaskPrincipal -UserId 'LocalService'
- plugins enable: converges the principal idempotently (migrating tasks
  an older installer or a dev box registered as SYSTEM) BEFORE starting,
  then grants LocalService read — via icacls, full-System32-path — on
  exactly the two SYSTEM/Admins-DACL'd files the runner's connect()
  needs: the scoped plugin-token and the TLS-pin cert.pem. Never
  mgmt-token. plugins disable revokes the grants; plugins status now
  prints the task principal so the migration is verifiable.
- build-scripting.ps1 mirrors the convergence + grants on dev deploys.

The usage text also mentions the new --allow-public-registry gate that
lands with the supply-chain commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 00:45:27 +02:00
enricobuehlerandClaude Fable 5 f0c511c8fa feat(plugins): punktfunk-host plugins CLI — add/remove/list/enable/disable/status
One-liner plugin management replacing the manual scripting-dir + bunfig +
bun-add ritual: package ops forward to the bun runner (new sdk plugins
module + runner-cli subcommands, 11 tests green), enable/disable/status
drive the systemd unit on Linux and the PunktfunkScripting scheduled task
on Windows (installer support in the ISS). Docs page rewritten as .mdx
with per-platform Tabs (registered in mdx.tsx).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 16:34:43 +02:00
enricobuehlerandClaude Opus 4.8 e7a8cf2bba feat(packaging): nix web console + plugin/script runner everywhere; fix tray zbus/tokio crash
ci / web (push) Successful in 58s
apple / swift (push) Successful in 1m24s
ci / docs-site (push) Successful in 59s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 17s
decky / build-publish (push) Successful in 25s
windows-host / package (push) Successful in 19m46s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 17s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 17s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 18s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 17s
android / android (push) Successful in 16m12s
arch / build-publish (push) Successful in 12m14s
ci / bench (push) Successful in 5m27s
apple / screenshots (push) Successful in 6m32s
ci / rust (push) Successful in 19m26s
deb / build-publish (push) Successful in 12m58s
docker / deploy-docs (push) Successful in 24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 24m51s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 24m51s
Ship the management web console and the plugin/script runner across the packaging
matrix, and fix a Linux tray startup crash.

- nix: add punktfunk-web (Nitro SSR on bun) and punktfunk-scripting (Effect runner),
  with services.punktfunk.{web,scripting} modules. Both bun node_modules come from
  fixed-output derivations pinned to real deps hashes; patchShebangs the web
  node_modules so orval/paraglide/vite run in the sandbox. Validated on x86_64-linux
  (nix build .#punktfunk-{web,scripting,tray,host} all green).

- punktfunk-scripting now ships for deb/rpm/arch/nix/windows, mirroring the web-console
  mechanics (vendored bun on deb/rpm/arch, pkgs.bun on nix; one self-contained
  `bun build --target=bun` bundle). Opt-in: host Recommends it, but the systemd --user
  unit ships un-started (Windows registers its scheduled task disabled).

- fix(tray): build punktfunk-tray in its OWN cargo invocation on nix/rpm/arch. Cargo
  unifies features across one `cargo build`, so co-building the tray with the host pulls
  the host's ashpd->zbus/tokio onto the tray's shared zbus and it panics at startup
  ("there is no reactor running, must be called from the context of a Tokio 1.x
  runtime"). Isolated, the tray's zbus stays on async-io. The .deb already did this;
  runtime-verified on nix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 21:16:34 +02:00
enricobuehlerandClaude Fable 5 9da38b8d9c ci(windows-host): build + clippy with the native QSV feature; encode-crate path triggers; CMake 4 policy floor
--features nvenc,amf-qsv,qsv on build and clippy (static VPL dispatcher, no new
DLLs — ffmpeg *_qsv stays as the fallback until Phase 4). Adds pf-encode/** +
libvpl-sys/** to the paths trigger (drift from the W6 split: encoder changes
only reached this workflow via Cargo.lock luck). Exports
CMAKE_POLICY_VERSION_MINIMUM=3.5: audiopus_sys' vendored opus fails a FRESH
CMake-4 configure (observed on this runner on a clean target dir), so green
runs were one cache purge from breaking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 20:01:05 +02:00
enricobuehlerandClaude Fable 5 600693914f fix(security): land the 2026-07 audit fixes — SSRF guards, roster lane, SYSTEM path hygiene
The low/medium findings from the July host+Windows security review, as
implemented in the audit session's working tree:

- Webhooks and library-art fetches refuse loopback/link-local/metadata
  targets and no longer follow redirects (SSRF pivots from the privileged
  host process).
- The paired-client rosters (/clients, /native/clients) move off the
  streaming-client auth lane — one paired device could enumerate every other
  device's name + fingerprint; only the bearer/loopback console keeps them.
- Device-name sanitizing extends to bidi/format control characters
  (spoofable rendering) via the shared native_pairing::is_spoofy_char;
  stream-marker quoting uses the same set.
- The SYSTEM service resolves powershell by its full System32 path —
  CreateProcess checks the launching EXE's own directory first, so a planted
  powershell.exe beside the host binary would have run as SYSTEM.
- The pf-vdisplay driver's opt-in file log moves from world-writable
  C:\Users\Public to WUDFHost's own temp dir.
- GameStream pairing sessions are single-use (removed whatever the outcome).
- Uninstall also removes the pf_mouse driver-store entry (rider from the
  virtual-HID-mouse work).
- openapi.json regenerated (hardened-config-dir doc wording).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 17:20:15 +02:00
enricobuehlerandClaude Opus 4.8 09849906e9 Merge perf/first-frame-latency: driver proto v4 + first-frame/resize latency (P0-P2)
Brings the first-frame-latency branch (P0.1 transition tracing, P1.1/P1.2
Welcome-time display prep, P2 in-place resize; pf-driver-proto v3 -> v4 with
IOCTL_UPDATE_MODES) onto current main. The branch predates the W6.2/W7 splits,
so git's rename detection carried most of it into the moved crates
(pf-capture idd_push, pf-vdisplay manager/pf_vdisplay, pf-win-display,
pf-driver-proto, the driver workspace) and the punktfunk1.rs remainder was
re-homed by hand:

- native/handshake.rs: welcome/start trace marks + the Welcome-time display
  prep spawn (the prep thread BECOMES the stream thread; hand-off via a
  SyncSender<SessionContext>). negotiate() gains bringup/quit/stop and returns
  the PrepHandle.
- native.rs: bringup/resize_ms creation + the stop/quit flags hoisted BEFORE
  the handshake (the close watcher splits: flags pre-handshake, lifecycle
  events post-handshake where `hello` exists); punch_done stamp; the data
  plane adopts the prep thread's result or builds inline.
- native/stream.rs: SessionContext/SendStats carry the trace; send_loop
  finishes it on the first video packet; the resize path gains the in-place
  fast path (try_inplace_resize) with the full rebuild as fallback, restructured
  so both share the post-rebuild bookkeeping; prepare_display/PreparedDisplay/
  PrepHandle; build_pipeline(+retry) thread the stage marks.
- session_status/mgmt: ttff_ms + last_resize_ms per session (union with the
  lifecycle-events fields main added to the same spots).
- pf-capture: Capturer gains capture_target_id() + resize_output() defaults.
- pf-vdisplay manager: perf's faster activation poll (60x50ms) + the settle
  floor before the PnP sweep, on main's knobs/no-trait shape.

Also: packaging/windows/build-gamepad-drivers.ps1 is ASCII again (an em-dash
from the pf-mouse work tripped windows-host.yml's locale-safety gate on main).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 16:08:16 +02:00
enricobuehlerandClaude Fable 5 9e6fc6e071 fix(host/inject,drivers): rumble root fixes A-C — lossless report ring + rumble-keyed idle watchdogs
B: PadFeedback.game_drove -> rumble_drove, keyed on vibration-asserting reports — an
LED/adaptive-trigger stream can no longer feed the abandoned-rumble force-off while a
coalesced stop never re-asserts (the confirmed unbounded stuck-ON path). C: Linux parity —
every UHID backend now arms the shared watchdog (Steam Input drives these pads over hidraw
with Windows abandonment semantics) and the uinput mixer force-stops abandoned
infinite-replay FF effects (FfState, unit-tested). Shared PUNKTFUNK_RUMBLE_IDLE_MS hatch
(0 = off; non-zero floored above SDL's ~2 s rumble resend).

A: PadShm v2.1 — a 1024 B tail extension carrying an 8-slot lossless output-report ring,
feature-negotiated via zeroed reserved fields (out_ring_ver; deliberately NO
GAMEPAD_PROTO_VERSION bump — mixed generations degrade to the legacy latest-report slot
instead of failing closed). The pf-dualsense driver dual-writes both planes
(publish_output); the host's shared OutputDrain drains oldest->newest with a torn-read
recheck and an overflow->resync path (PadFeedback.resync force-stops + re-arms dedups).
pf-umdf-util grows a min_data_size map fallback. Ds*Feedback.fresh removed (dead).

design/rumble-root-fix.md par. A-C. Verified: pf-inject tests+clippy Linux+Windows (53/53
on winbox incl. the stop-coalesce repro); drivers ws check+clippy on the CI runner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 14:07:32 +02:00
enricobuehlerandClaude Fable 5 85dd2bb077 feat(host/windows): resident virtual HID mouse (pf-mouse UMDF minidriver)
Headless Windows hosts (no dongle) stream an INVISIBLE cursor: with no
pointing device present win32k reports SM_MOUSEPRESENT=0 and DWM never
composites a pointer into the pf-vdisplay frame, even though SendInput
moves it. Keep ONE virtual HID mouse devnode alive for the host's
lifetime — the Sunshine/Parsec-class fix, zero client changes.

- pf-mouse: UMDF2 HID minidriver, one fixed identity (PF:MO 5046:4D4F,
  obviously virtual, nothing fingerprints it), one 8-byte input report
  (5 buttons + absolute 15-bit X/Y + wheel + AC-pan). Transport is the
  sealed pad channel verbatim (Global\pfmouse-boot-0 mailbox + unnamed
  MouseShm DATA section) so pf-umdf-util's audited layer serves it
  unchanged; report delivery is event-driven (idle = no HID traffic).
- host: inject::mouse_windows — VirtualMouse (SwDeviceCreate'd devnode +
  channel), ensure_resident() keeper thread started by every
  InjectorService (process-wide, PUNKTFUNK_NO_VIRTUAL_MOUSE opts out),
  vmouse-spike on-glass validation (cursor sweep via HID reports).
- proto: mouse module (magic, boot-name, identity, report layout,
  unit-tested input_report packing).
- SwDeviceProfile grows container_tag so the mouse's ContainerId family
  (PFMO) never groups with a pad's (PFDS) in the Devices UI.
- packaging: pf-mouse rides the gamepad-driver build + install pipeline
  (build-gamepad-drivers.ps1, windows-drivers.yml, driver install
  --gamepad picks up every staged .inf).

On-glass validated on winbox: devnode + HID child bind, SM_MOUSEPRESENT=1
with no physical mouse, cursor sweeps via HID reports (vmouse-spike).

This work was implemented in a parallel session; committed here as the
build prerequisite for the HID compose kick that follows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 11:02:10 +02:00
enricobuehlerandClaude Fable 5 3d9b329084 fix(host): name the lid-closed/no-frames failure — display-write decode, console-session guard, driver-truth attach diagnostics
Field report (Windows laptop, lid closed, Tailscale): v0.12.0's activation
fix works — the pf-vdisplay target activates in ~200ms — but the session
still dies at the first-frame gate: 'driver_status=1 but no frame published
within 4s'. Triage showed three independent blind spots; this names all of
them at their source instead of guessing downstream:

- pf-win-display: decode ChangeDisplaySettingsExW failures (-1 FAILED — a
  display write rejected, the wrong/remote-session signature — vs -2 BADMODE,
  which the old 'mode not advertised?' text conflated), and WARN on every
  non-zero SetDisplayConfig rc in the CCD isolate even when verification
  passes vacuously (the lid-closed case: nothing else active, so the INFO
  swallowed rc=0x5 ERROR_ACCESS_DENIED while the load-bearing COMMIT_MODES →
  ASSIGN_SWAPCHAIN re-commit silently never applied). Access-denied rcs get
  the remedy appended (console session / installed service).

- host: console-session guard (interactive::console_session_mismatch) — a
  host outside the active console session (a hand-launched host after an RDP
  round-trip) fails every display write, reads the wrong session's GDI view,
  and its SendInput compose kicks go nowhere. Named ERROR at vdisplay
  acquire + appended to the first-frame timeout, instead of the misleading
  generic failure. (The idd_push diagnosis half of this landed in 9a36ea21;
  this commit adds the proto helpers + session guard it references, healing
  the windows-cfg build.)

- proto + driver: while OPENED, driver_status_detail now carries a live
  packed word (bit31 live-marker | offered 15-bit | mismatch-dropped 16-bit)
  maintained by the publisher, so the host's first-frame timeout can tell
  apart: never-attached (no swap-chain worker ran), attached-but-DWM-composed-
  zero-frames (undamaged/powered-off desktop, kicks blocked on the secure
  desktop), and composed-but-every-frame-mismatched (ring sized from a stale/
  foreign-session GDI mode). Zero layout change, old drivers read as 'no
  detail'; unit-tested pack/unpack in pf-driver-proto.

Verified on winbox: cargo check + clippy -p punktfunk-host -p pf-win-display
-p pf-driver-proto EXIT 0, drivers ws cargo check -p pf-vdisplay EXIT 0
(Version_Number=10.0.26100.0), cargo fmt --all --check clean; pf-driver-proto
tests 13/13 pass locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 10:47:47 +02:00
enricobuehlerandClaude Fable 5 45c29a99d5 perf(host+driver): in-place resize = advertised-mode fast path + mode-history union
On-glass round 2 settled the mechanism: after UpdateModes2 the OS re-parses
our description AND re-queries target modes (driver log — both callbacks
served the fresh list) yet the SETTABLE set stays pruned to the modes known
at monitor ARRIVAL; the monitor source-mode set is pinned then, below
anything the driver can refresh. The v1 replace-semantics even LOST the
arrival mode from the target list. Consequences:

- driver: UPDATE_MODES now UNIONs (new mode first, previous list kept,
  deduped by resolution, cap 12), and a re-created same-id monitor inherits
  its departed predecessor's list (MODE_HISTORY) — every size an identity
  ever served is settable at the next arrival, so returning to a
  previously-used size (windowed<->fullscreen, drag back) is IN-PLACE.
- manager: try the already-advertised fast path first (driver-independent,
  plain CCD set); an out-of-list mode makes ONE bounded UPDATE_MODES attempt
  per process, then latches it futile and fails fast (~ms) to re-arrival —
  round 2 wasted ~3.1 s per arbitrary resize on the doomed wait. Fallback
  log demoted warn->info (expected-normal for first-seen sizes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 18:01:36 +02:00
enricobuehlerandClaude Fable 5 a738de6cd8 fix(host): force a CCD mode re-enumeration after UPDATE_MODES (in-place resize)
First on-glass run: the driver accepted every UpdateModes2 (0x0 in the driver
log) but the OS never re-enumerated the target's settable modes on its own —
'OS did not advertise 800x1050 within 2s' → re-arrival fallback every time.
Re-commit the current config with SDC_FORCE_MODE_ENUMERATION (the same nudge
the isolate/layout paths already rely on) before the advertised-wait, re-kick
up to 3x, and log the actually-offered resolutions when it still misses.
Driver: dbglog the *2 mode-query/parse callbacks so the re-enumeration story
is visible in pfvd-driver.log.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:48:44 +02:00
enricobuehlerandClaude Fable 5 0899e53903 feat(driver): pf-vdisplay IOCTL_UPDATE_MODES — live monitor mode-list refresh (proto v4)
Latency plan P2.1 (design/first-frame-and-resize-latency.md): a new additive
control-plane op lets the host refresh a LIVE monitor's advertised target-mode
list to lead with an arbitrary new mode (IddCxMonitorUpdateModes2 — the same
IddCx 1.10 *2 family this driver already requires, so no new OS floor). This
removes the 'mode list frozen at ADD' constraint that forced the mid-stream
resize through a REMOVE->ADD monitor hotplug: the monitor's OS identity, its
swap-chain worker and the retained FrameStash all survive an in-place mode set.

Protocol v4 is ADDITIVE over v3: the host's handshake floor stays at v3
(MIN_DRIVER_PROTOCOL_VERSION) and gates the in-place path on the reported
version, keeping re-arrival as the permanent fallback. The driver's stored
mode list is swapped before the DDI and reverted if it fails, so the OS and
the mode-DDI callbacks always agree.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 17:12:13 +02:00
enricobuehlerandClaude Fable 5 ce085b8e3b style(vdisplay): dedupe the attach-block comment (first-frame stash follow-up)
Comment-only: the lazy-attach comment carried the delivery-consumption
sentence twice after the stash rework.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 15:24:36 +02:00
enricobuehlerandClaude Fable 5 89a08f83af fix(vdisplay): first-frame guarantee — republish a retained frame at ring attach
DWM composes a display only when something dirties it, so a session opened
onto an idle desktop never produced a first frame: the host's synthetic-input
"compose kick" (cursor wiggle / sibling-display jump) was the only source, and
it is inherently unreliable — blocked on the secure desktop, defeated by a
fullscreen game's ClipCursor, user-visible, and dead in service contexts. The
field symptom: connect → black stream until something repaints the desktop.

Reconstruct DDA's first-frame semantics at the driver instead (DDA seeds a new
duplication with the current desktop image; IDD-push never had an equivalent):

* frame_transport.rs: new FrameStash — the retained last composed frame, a
  driver-private copy-only texture. publish() now reports Published /
  DescMismatch / Dropped, and harvest_into() pulls the last-published ring
  slot into the stash (keyed-mutex guarded, freshness-checked) before a
  superseded publisher is dropped — between sessions the driver keeps writing
  the host-side-dead old ring, so that slot IS the current desktop image.
* swap_chain_processor.rs: the worker stashes every frame the ring can NOT
  take (unattached, or descriptor-mismatched during a mode/HDR-flip race),
  harvests before a supersede, and REPUBLISHES the stash into every freshly
  attached ring — the host sees a normal seq=1 publish milliseconds after
  channel delivery, no compose needed. Zero steady-state cost: matched
  publishes touch only the ring. The frame-channel stash is now polled every
  iteration (attach latency = first-frame latency; it was 1-in-30).
* monitor.rs: preserved_stash (LUID-tagged) so the retained frame survives
  swap-chain unassign→reassign flaps, alongside the preserved publisher.
* host idd_push.rs: kick_dwm_compose demoted to documented last-resort
  fallback for pre-stash drivers; a debug log now fires when a kick actually
  runs so field logs show whether the stash path is working.

No proto change: the republish is an ordinary publish, so old host + new
driver and new host + old driver both keep working (the latter via the kick).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 15:16:36 +02:00
enricobuehlerandClaude Fable 5 11045a0f70 chore: consolidate parallel-session WIP (HOLD — do not push)
Local snapshot of intermingled in-flight work, committed to unblock the encode
refactor (a clean ffmpeg_win.rs for the vbv-dedup follow-on). These hunks span
the same files and can't be cleanly split here; the commit bundles three
distinct workstreams that each belong in their own PR:

  - logging rework (~43 files: level re-tiering, structured fields, `?e`,
    hot-path flood latches)
  - conflicting-host detection (detect.rs + detect/{linux,windows}.rs + wiring
    in main.rs/mgmt.rs/Cargo.toml/docs/packaging)
  - standby-sink DWM-stall attribution (windows/display_events.rs + capture/
    vdisplay wiring)

NOT verified as a combination. NOT to be pushed until the refactor is done and
these are re-verified and reorganized into their proper per-workstream PRs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 12:42:53 +02:00
enricobuehlerandClaude Fable 5 32e5594a9a fix(drivers): per-pad MAC + USB serial in pf-dualsense — SDL/Steam dedup by serial
ci / web (push) Successful in 59s
ci / docs-site (push) Successful in 1m8s
decky / build-publish (push) Successful in 17s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 7s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 8s
ci / bench (push) Successful in 5m31s
arch / build-publish (push) Successful in 10m59s
android / android (push) Successful in 13m54s
windows-drivers / probe-and-proto (push) Successful in 23s
windows-drivers / driver-build (push) Successful in 1m27s
ci / rust (push) Successful in 17m32s
deb / build-publish (push) Successful in 11m37s
docker / deploy-docs (push) Successful in 51s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m36s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 19m24s
windows-host / package (push) Successful in 14m21s
apple / swift (push) Has been cancelled
apple / screenshots (push) Has been cancelled
Windows counterpart of 5c7e0afa's Linux per-pad pairing MAC: every virtual
DualSense / Edge / DualShock 4 presented ONE hardcoded serial, so SDL/Steam
(which dedup controllers by serial) could merge a second pad into the first.

* GET_FEATURE pairing replies (DS/Edge 0x09, DS4 0x12) now carry the pad
  index the host stamps into the sealed section in the MAC's low octet.
* GET_STRING serial strings (HidD_GetSerialNumberString — what SDL actually
  reads on Windows) get the same per-pad low octet, agreeing with the
  feature MAC. The Edge's 0x09 reply moves onto its serial-string base
  (0x75 = DS base + 1), fixing the pre-existing feature-vs-string mismatch.
* The Deck identity already did this per-pad; its two inline index reads
  now share the new `pad_index()` helper.

Pad 0 keeps today's serial values for DS / DS4 / Deck (no identity churn
for existing single-pad setups).

Verified on the windows-amd64 runner: cargo build + clippy -D warnings
(pf-umdf-util / pf-xusb / pf-dualsense) + fmt clean on the pinned 1.96.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 23:31:40 +02:00
enricobuehlerandClaude Fable 5 f3b6ccaa7f fix(gamepad/windows): Steam-accepted Deck unit serial un-mangles the controller name
Steam validates the Deck unit serial's format before accepting it. Our
"PFDK..." serial was REJECTED ("Invalid or missing unit serial number"), so
Steam substituted a hash identity and mangled the displayed name to
"Steam Deck Controllerggg" on every host tested. An 'F'-leading serial passes,
so switch to "FVPF..." — keeps the PunktFunk marker one slot in, still distinct
from a real Deck's "FVZZ..." for the Linux self-detection in
physical_steam_controller_present(). The name now shows a clean "Steam Deck
Controller" with a serial-derived handle (verified on .173).

Also fix the UMDF driver's 0xAE GET_STRING_ATTRIBUTE handler to echo the
requested attribute id faithfully instead of collapsing board-serial (0x00)
requests to unit-serial (0x01). Steam still logs a benign "Deck Controller PCB
Serial# invalid" for the board serial — it validates that against a
Valve-internal format for ANY value, including an empty one (verified) — but
that line does not mangle the name, change the handle, or block promotion.

Applied to both transports: host inject/proto/steam_proto.rs::deck_serial
(Linux gadget/usbip) and the pf-dualsense UMDF driver (Windows), which mirror
each other's serial format.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 16:31:07 +02:00
enricobuehlerandClaude Fable 5 d8e8529cd7 feat(gamepad): Windows Steam Deck backend — Steam-Input-promoted UMDF virtual Deck
windows-drivers / probe-and-proto (push) Successful in 18s
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows-host / package (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
release / apple (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
decky / build-publish (push) Has been cancelled
deb / build-publish (push) Has been cancelled
ci / web (push) Successful in 51s
windows-drivers / driver-build (push) Successful in 1m51s
ci / docs-site (push) Successful in 54s
ci / bench (push) Successful in 5m20s
arch / build-publish (push) Successful in 13m32s
android / android (push) Successful in 15m7s
ci / rust (push) Successful in 21m11s
apple / swift (push) Successful in 4m59s
apple / screenshots (push) Successful in 20m53s
The N4 GO verdict, productized. GamepadPref::SteamDeck on a Windows host
now builds a real virtual Deck instead of folding to DualSense: games
get native Deck glyphs + both trackpads + gyro + all four back grips
through Steam Input's own remapping.

- steam_deck_windows.rs: DeckWinPad/DeckWinProto/SteamDeckWindowsManager
  over the sealed shm channel, sharing the whole Linux Deck codec
  (steam_proto now compiles on Windows too — it was already pure). The
  SwDevice identity carries usb_mi: Some(2): the &MI_02 hardware-id
  token hidclass mirrors into the HID child and Steam parses as the
  wired controller interface — the promotion gate.
- Driver: DEVTYPE_STEAMDECK (3) graduates from the spike — SET_FEATURE
  0xEB rumble / 0x8F haptic pulses are republished to the host through
  the output slot (report-id-0 prefixed, so parse_steam_output sees the
  Linux wire shape), and the 0xAE/GET_STRING serial + 0x83 unit id are
  per-pad (read from the section's pad_index; PFDK<unit-id> matches
  steam_proto::deck_serial).
- Router: SteamDeck arms in the Windows Pads paths; pick_gamepad flips
  SteamDeck-if-windows -> SteamDeck (the DualSense fold retires);
  dualsense-windows-test grows --deck.

ON-GLASS VALIDATED on .173 (rebuilt signed driver 9.9.0714.12xx
installed, Steam live): the manager-created pad (index 1) enumerates
with per-pad serial PFDK50460001, Steam logs Interface: 2 ->
'!! Steam controller device opened' -> 'Steam Controller reserving
XInput slot 0' -> PollState 2 (actively polling our cycling input
frames) -> mapping activated; clean teardown on exit. Rumble round-trip
through a real game remains an on-glass debt (nothing sent 0xEB during
the idle hold).

Known gap vs Linux: no physical-Steam-controller conflict degrade on
Windows yet (degrade_steam_on_conflict is Linux-only — /sys scan); a
Windows equivalent needs SetupDi enumeration and is deferred.

Verified: .21 clippy -D warnings + 304/0 tests + fmt --all; .133 clippy
-D warnings + the WDK driver-workspace check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 12:36:26 +02:00
enricobuehlerandClaude Fable 5 69f30f30b6 style(pf-dualsense): rustfmt the N4-spike additions (CI fmt gate)
windows-drivers / probe-and-proto (push) Successful in 18s
windows-drivers / driver-build (push) Successful in 1m37s
windows-host / package (push) Successful in 9m1s
ci / rust (push) Failing after 40s
ci / web (push) Successful in 55s
ci / docs-site (push) Successful in 1m7s
decky / build-publish (push) Successful in 24s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 10s
arch / build-publish (push) Successful in 10m21s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 7s
ci / bench (push) Successful in 5m20s
android / android (push) Successful in 16m58s
apple / swift (push) Successful in 4m31s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 7m19s
deb / build-publish (push) Successful in 11m39s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 13m8s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 16m27s
docker / deploy-docs (push) Successful in 10s
apple / screenshots (push) Successful in 20m45s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:55:47 +02:00
enricobuehlerandClaude Fable 5 8d8168b0e0 feat(gamepad): N4 spike kit — software-devnode Steam Deck probe for Windows
The gamepad-new-types §6 go/no-go rig, ready to run the moment .173 is
back (the box is currently down, so the observation itself is still
owed): does Steam Input on Windows promote a software-devnode HID Deck
(28DE:1205), or does it need a real USB bus identity (the documented
GameInput instance-path gap — the Linux 'Interface: -1' lesson)?

- Driver: scratch device_type=3 serves the Deck identity — the captured
  38-byte controller-interface descriptor, 28DE:1205 attributes, Valve
  strings, the Deck neutral frame, and the Steam 0x83/0xAE feature
  contract (SET_FEATURE latches the command, GET_FEATURE answers it —
  attribute blob + unit serial mirroring steam_proto::feature_reply).
  Never stamped by a session. INF gains pf_steamdeck.
- Host: deck_spike_hold() + the `deck-windows-spike` subcommand — stamps
  devtype 3, spawns the devnode under VID_28DE&PID_1205, streams the
  neutral frame, prints what to observe (Steam logs/controller.txt,
  controller settings) and logs any output reports Steam writes.

Run recipe (on .173, once the updated signed driver is staged): install
driver, start Steam, `punktfunk-host.exe deck-windows-spike`, watch
controller.txt. GO -> plan a proper N4 phase (the Deck codec is already
shared); NO-GO -> document next to the Linux Interface:-1 note and keep
the SteamDeck->DualSense Windows fold.

Verified: .133 clippy -D warnings + the driver workspace cargo check
(WDK) both green; .21 clippy + 304/0 tests unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:44:59 +02:00
enricobuehlerandClaude Fable 5 1830e095f8 feat(gamepad): DualSense Edge backend — Linux UHID + Windows UMDF (N1)
The plain-DualSense transport + report codec under the Edge USB identity
(054C:0DF2, verbatim 389-byte real-device descriptor cross-checked against
the raw usbmon capture + hhd's production virtual Edge), so the wire back
grips (BTN_PADDLE1..4: Deck L4/L5/R4/R5, Elite P1-P4) land on the Edge's
NATIVE buttons[2] bits instead of the fold/drop policy: PADDLE1/2 -> the
right/left back buttons, PADDLE3/4 -> the right/left Fn buttons (kernel
BTN_TRIGGER_HAPPY1..4 on >= 7.2; SDL/Steam read hidraw on any kernel).

- proto: Edge descriptor + btn2 bits + edge_paddle_bits(), pinned against
  hid-playstation DS_EDGE_BUTTONS_* and SDL_hidapi_ps5 (tests).
- Linux: DsUhidIdentity parameterizes the UHID create; DsEdgeLinuxProto /
  DualSenseEdgeManager. Headless-validated on .21 (7.1): driver=playstation
  binds 0DF2, all 4 input devices created, probe lightbar/player-LED
  feedback round-trips; dualsense-test grew --edge (cycles all 4 paddles).
- Windows: UMDF driver serves device_type=2 (Edge descriptor/attrs/strings,
  DS feature blobs); WinDsIdentity parameterizes the SwDevice profile +
  devtype stamp; DsEdgeWinProto / DualSenseEdgeWindowsManager; INF gains
  pf_dualsenseedge. Driver change => resign + reinstall before on-glass.
- Router: DualSenseEdge arms in route_handle/apply_rich/pump/heartbeat;
  pick_gamepad folds Edge -> itself on linux||windows; degrade_if_no_uhid
  covers it.
- Client (SDL): 054C:0DF2 declares DualSenseEdge (no distinct SDL type);
  Edge physical pads take the raw DS5 effects path; console-UI glyphs =
  Shapes. Apple/Android pickers follow separately.

Verified: .21 clippy -D warnings + 292/0 host tests + on-box UHID bind
smoke; .133 clippy pending in this push.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 10:49:31 +02:00
enricobuehlerandClaude Opus 4.8 cd701a9594 fix(vdisplay): preserve FramePublisher across swap-chain reassign (sibling-join freeze)
windows-drivers / probe-and-proto (push) Successful in 28s
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m0s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 19s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 8s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 9s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 7s
windows-drivers / driver-build (push) Successful in 1m35s
apple / swift (push) Successful in 4m34s
ci / bench (push) Successful in 5m39s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 6m50s
docker / deploy-docs (push) Successful in 33s
windows-host / package (push) Successful in 10m8s
android / android (push) Successful in 12m24s
arch / build-publish (push) Successful in 16m41s
deb / build-publish (push) Successful in 17m40s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 12m13s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m21s
ci / rust (push) Successful in 23m52s
apple / screenshots (push) Successful in 20m9s
When a second client got its own virtual display mid-stream, the FIRST
client's IDD-push stream froze (video only; `new_fps=0 repeat_fps=240`
forever). Adding/removing/resizing a sibling display re-commits the CCD
topology, which makes the OS unassign+reassign the first monitor's IddCx
swap chain. `unassign_swap_chain` dropped the SwapChainProcessor, dropping
`run_core`'s local FramePublisher and closing the sealed-channel handles.
The fresh worker then polled the frame-channel stash — but that stash is
consumed once at session open, and the host only re-delivers on a ring
recreate (a descriptor change). The first monitor's descriptor didn't
change and WUDFHost stayed alive, so no watchdog fired: the driver drained
the swap chain without publishing and the host repeated its last frame
indefinitely. Confirmed twice on the .173 box (host.log 21:12 & 21:15).

Preserve the live FramePublisher across the flap instead of dropping it:
the host-owned ring (header/event/textures) it holds stays valid — only
the swap chain died.

- frame_transport.rs: FramePublisher records its render-adapter LUID +
  exposes render_adapter().
- monitor.rs: MonitorObject.preserved_publisher + preserve_publisher()
  (mirrors set_frame_channel) + take_preserved_publisher() (mirrors
  take_frame_channel). Monitor teardown drops the stashed publisher and
  closes its ring handles, so nothing leaks.
- swap_chain_processor.rs run_core: after SetDevice OK, re-adopt a
  preserved publisher ONLY when the new swap chain renders on the same
  LUID (same pooled Direct3DDevice → its context + opened textures are
  valid); on loop exit, stash the publisher back on the monitor.

Safe: the old worker is fully joined (drop-outside-lock discipline)
before the new one runs, so no concurrent context use; a stale re-adopted
publisher is superseded by the existing is_stale() + has_frame_channel()
newest-wins checks at the loop top.

Verified clippy -D warnings clean on rustc 1.96.0 via a faithful mock
crate (the real crate needs the WDK to compile). Needs a driver rebuild +
reinstall on the host to take effect; not yet hardware-validated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 00:19:20 +02:00
enricobuehlerandClaude Fable 5 35d97ae6ac feat(windows): parallel virtual displays — proto v3 ring binding, manager slot map, group topology (W0–W3)
windows-drivers / probe-and-proto (push) Successful in 41s
ci / web (push) Successful in 1m2s
ci / docs-site (push) Successful in 1m7s
apple / swift (push) Successful in 1m11s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 8s
decky / build-publish (push) Successful in 15s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 7s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 6s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 38s
windows-drivers / driver-build (push) Successful in 1m46s
ci / bench (push) Successful in 6m35s
docker / deploy-docs (push) Successful in 20s
windows-host / package (push) Successful in 9m37s
deb / build-publish (push) Successful in 13m48s
arch / build-publish (push) Successful in 14m21s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 2m24s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 13m34s
android / android (push) Successful in 15m45s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m20s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m18s
ci / rust (push) Successful in 18m48s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m41s
release / apple (push) Successful in 19m30s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m49s
flatpak / build-publish (push) Failing after 2m9s
apple / screenshots (push) Successful in 19m3s
design/windows-parallel-virtual-displays.md (display-management Stage 7 / §6.6): N
simultaneously-live pf-vdisplay monitors, one sealed ring each, every idd-push-security
invariant preserved per-ring.

- proto v3: SharedHeader._pad → target_id — the ring NAMES its monitor, host-stamped
  before the magic; the driver publisher refuses a cross-bound ring via the shared,
  unit-tested frame::check_attach (new DRV_STATUS_BIND_FAIL — the gamepad pad_index
  validation applied to frames, invariant #10); the host's wait_for_attach surfaces the
  refusal loudly and self-checks its own stamp.
- manager: the one-monitor MgrState becomes a slot map keyed by the client's identity
  slot (0 = anonymous/GameStream); per-slot reconnect + dead-WUDFHost preempts,
  slot-scoped begin_idd_setup (a different identity is an admission question, never a
  preempt), ONE device-level watchdog pinger, per-slot /display/state + /display/release.
- group topology: isolate_displays_ccd takes the managed target SET (a sibling slot is
  never deactivated); SavedConfig + the DDC/PnP axes move to the group record (first-in
  captures, last-out restores); desktop layout via CCD source origins from the pure
  layout::arrange (auto-row default, manual pins win), re-applied on create + reconfigure.
- admission: the Windows separate→reject override now sits behind the
  PUNKTFUNK_WIN_SEPARATE=1 validation hatch (the wedge it guarded is structurally gone —
  a second identity gets its own monitor + ring; default flips in W5 after soak);
  max_displays and NVENC session-unit budgets decline an unaffordable display AT
  admission; kick_dwm_compose is process-globally throttled and per-display — cursor
  jump + 35 ms dwell (a sub-tick jump composes nothing; DWM reads dirties from current
  state at the next vsync tick).

On-glass on the RTX box: V1/V2/V4/V5/V6/V9 green — two paired clients on two monitors
streaming ~60 fps each with zero mismatches and zero bind failures, churn-hammer clean
(no 0x80070490), per-ring mode-change recreate leaves the sibling untouched, typed
budget rejection, fault-injected cross-bind refused loudly with the sibling undisturbed.
V7: WUDFHost-kill shared fate is clean; in-process device recovery is a known follow-up
(the retired-never-closed control handles block the adapter cycle — reset-pf-vdisplay.ps1
recovers). DWM composes two IDD monitors concurrently at 60 fps — the plan's
load-bearing unknown, answered yes.

Also carries the client-HDR EDID forwarding that shared this working tree
(Hello::display_hdr → AddRequest luminance tail → the monitor's CTA-861.3 HDR block,
PUNKTFUNK_CLIENT_PEAK_NITS hatch) and the Deck client fixes (40 ms rumble keep-alive
with 1-LSB jitter, HDR self-diagnosing presenter warn, flatpak HDR env).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 01:06:44 +02:00
enricobuehlerandClaude Fable 5 ef39050dbc docs: repo-wide housekeeping — sync README & docs with the code as shipped
windows-drivers / probe-and-proto (push) Successful in 47s
ci / web (push) Successful in 58s
apple / swift (push) Successful in 1m13s
decky / build-publish (push) Successful in 16s
ci / docs-site (push) Successful in 1m19s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 8s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 43s
windows-drivers / driver-build (push) Successful in 1m45s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 55s
ci / bench (push) Successful in 7m18s
flatpak / build-publish (push) Successful in 6m18s
docker / deploy-docs (push) Successful in 25s
windows-host / package (push) Successful in 8m31s
release / apple (push) Successful in 11m41s
android / android (push) Successful in 13m4s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 1m57s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Successful in 2m16s
arch / build-publish (push) Successful in 16m24s
windows / build (aarch64-pc-windows-msvc) (push) Successful in 1m6s
deb / build-publish (push) Successful in 17m54s
windows / build (x86_64-pc-windows-msvc) (push) Successful in 1m27s
ci / rust (push) Successful in 18m23s
apple / screenshots (push) Successful in 5m58s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 20m24s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m43s
Six parallel audits swept the root docs, docs-site, every per-directory
README, and the packaging docs; every claim below was verified against
the source before editing.

- README: Layout gains the six missing crates (pf-client-core,
  pf-presenter, pf-console-ui, pf-ffvk, pf-driver-proto, punktfunk-tray),
  clients/session, api/ and ci/; Linux/Windows client rows reflect the
  shell + Vulkan-session split and the Vulkan Video -> VAAPI/D3D11VA ->
  software decode chains; the "every client over a C ABI" claim is
  corrected (Rust clients link the core directly); tiered stats overlay
  + console shell noted; Apple row mentions AV1.
- CONTRIBUTING: drop the dead CLAUDE.md link (deliberately untracked);
  point at the README's build/invariants sections. SECURITY: 0.9.0.
- host-cli/pairing: --allow-pairing/--require-pairing are no-op legacy
  names — pairing is required by default, --allow-tofu is the real flag;
  document --data-port and --idle-timeout-ms.
- configuration: document PUNKTFUNK_RECOVER_SESSION_CMD (session-crash
  recovery hook), PUNKTFUNK_MDNS, PUNKTFUNK_DATA_PORT.
- virtual-displays/gnome: GNOME per-client scaling shipped (host-
  persisted) — flip the  to  and describe how it works.
- stats: new "Detail levels" section (Off/Compact/Normal/Detailed +
  per-platform cycle gestures); retire the GTK hand-off note.
- clients/install-client/status/roadmap: decode chains, Windows client
  validation narrowed to HDR-only pending, adaptive bitrate, console
  shell, Apple AV1, Windows host vendor list.
- Sub-READMEs: clients/linux rewritten for the re-architecture; session
  Windows decode rung + d3d11va knob; Windows tiered overlay; Android
  minSdk 28; decky file table; host zerocopy/ path; scripts port
  47992 and steamos-host.md; pf-dualsense source path.
- packaging: canary version bases are tag-derived (<next-minor> via
  pf-version.sh/.ps1), codecs-extra not ffmpeg-full, document the
  pinned offline-Skia tarball + SKIA_BINARIES_URL and vulkan-headers.
- Convert 15 dangling design/*.md links to the punktfunk-planning
  prose convention (those docs live in the private planning repo).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 09:13:42 +02:00
enricobuehlerandClaude Fable 5 eaacdfccc2 fix(windows-drivers): pf-vdisplay robustness — AdapterInitStatus gate, pooled-device TDR check, MMCSS-optional worker
Batch B of the audit's medium tier (M4+M5+M6):

- M4: adapter_init_finished now reads AdapterInitStatus (was ignored) and
  only stashes the adapter on NT_SUCCESS, per the MS sample. A failed async
  init previously produced a HUSK adapter: monitors created on it arrive
  but the OS never assigns a swap-chain — every session black-screens with
  no visible cause (the exact signature live fault-injection produced after
  a WUDFHost kill). Unset adapter → ADD fails cleanly (host-retryable) and
  a re-entrant D0 retries the init; the status is now in the debug log.
- M5: pooled_device checks GetDeviceRemovedReason on a cache hit — a TDR'd
  device was returned for its LUID forever (SetDevice fail-loop, black
  virtual display until device teardown); now it falls through to a fresh
  create.
- M6: an AvSetMmThreadCharacteristicsW failure no longer aborts the worker
  before draining (which stalled the monitor and leaked the WDF swap-chain
  object) — continue unprioritized like the MS sample; revert only if MMCSS
  actually engaged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 17:20:48 +00:00
enricobuehlerandClaude Fable 5 ea8e996989 fix(windows): IDD-push audit highs — keyed-mutex timeout, two per-frame leaks, IDD_PUSH knob, pooled-device threading
Five verified findings from the IDD-push/pf-vdisplay deep audit:

- Keyed-mutex acquire (BOTH endpoints): AcquireSync returns WAIT_TIMEOUT
  (0x102) / WAIT_ABANDONED (0x80) as SUCCESS-severity HRESULTs, which the
  windows-rs Result wrapper erases — a busy slot read as "acquired", so
  driver and host could race the same ring texture (torn frames) and the
  designed busy-skip backpressure was dead code. Both sides now classify
  the raw vtable HRESULT; WAIT_ABANDONED counts as acquired (ownership
  transfers — refusing it would wedge the slot forever).
- Host SDR hot path leaked one ID3D11VideoProcessorInputView per converted
  frame: the D3D11_VIDEO_PROCESSOR_STREAM ManuallyDrop field suppressed the
  release after VideoProcessorBlt. Released by hand now, success or not.
- Driver leaked IddCx's per-acquire surface reference (from_raw_borrowed on
  a TRANSFERRED reference — the MS sample Attach/Reset's it): the swap-chain
  surface set survived swap-chain destruction, the likely true root cause of
  the ~50 MB-per-reconnect VRAM loss that device pooling only mitigated.
  Now adopted via from_raw (publisher or not) and dropped pre-Finished.
- PUNKTFUNK_IDD_PUSH removed: capture is unconditionally IDD-push, but the
  vdisplay manager still gated the lingering-monitor preempt (and render
  pin) on the knob, whose default was OFF — dev/CLI runs reused a lingering
  monitor whose IddCx swap-chain is dead (black reconnect). The preempt and
  the render-GPU pin are now unconditional; host.env comments no longer
  promise the removed DDA/WGC fallback.
- Driver D3D device: dropped D3D11_CREATE_DEVICE_SINGLETHREADED (unsound
  since DEVICE_POOL shares one device across processors) and the pooled
  immediate context is now SetMultithreadProtected — two concurrent
  monitors' workers otherwise race an unlocked context (UB in the UMD).

No wire-contract change (pf-driver-proto untouched); the driver fixes take
effect on the next pf-vdisplay redeploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:27:13 +00:00
enricobuehlerandClaude Fable 5 5ee474c9e1 fix(packaging/windows): Inno [Code] comment brace-nesting trap broke ISCC
The PublicFwParam doc comment contained a literal code-constant token; Inno's
{ } comments don't nest, so its closing brace ended the comment early and the
trailing text parsed as code ("'BEGIN' expected", compile aborted). Reworded to
avoid the literal braces + added a warning note. Verified: the [Code] section
has no other nested-brace-in-comment traps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:27:27 +00:00
enricobuehlerandClaude Opus 4.8 be6c71aed6 feat(security): finish Windows firewall Public opt-in wiring + vuln-disclosure + doc cleanup
Firewall (the service.rs core landed in 796d381): scope the web-console rule
(TCP 47992) to Domain+Private by default with a `--allow-public-network` opt-in
that deletes-then-re-adds the rule, and add the installer "Allow connections on
Public networks" task (unchecked) forwarding the flag to `service install` and
`web setup`. Default is now trusted-networks-only; Public is explicit.

Vulnerability disclosure: SECURITY.md (report to security@punktfunk.com, scope,
SLAs, safe harbor), a Gitea issue-template contact link, a README security line,
and a Reporting section on the docs Security page.

Docs: the Security page now documents the Private/Domain firewall default (and
how to fix a misclassified-Public network / opt in); removed internal design-doc
and CLAUDE.md links from the user-facing docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 14:08:17 +00:00
enricobuehlerandClaude Fable 5 796d381926 fix(windows): opt-in pad-driver file logs + size-capped service log rotation
Two disk-write fixes:

- pf-xusb/pf-dualsense no longer write C:\Users\Public\pf*-driver.log
  unconditionally — the file log is now opt-in (debug builds, or the
  PFXUSB_DEBUG_LOG / PFDS_DEBUG_LOG system env var), mirroring the audit-§4.4
  fix pf-vdisplay already got: a release driver never writes the world-writable
  Public file (info-leak/DoS surface), and the per-report OUTPUT/SET_STATE hex
  dumps stop being a sustained per-rumble disk-write path during gameplay.
  OutputDebugStringA stays unconditional; the host's driver-silence WARN and
  the gamepad-driver-health failure-mode table now say the log is opt-in.

- service.log/host.log get one-generation rotation: at each (re)open a file
  over 10 MB is renamed to .old, so a crash-restart loop or a RUST_LOG=debug
  left in host.env can't grow the append-forever logs without bound. Rotation
  runs only before an open (never under a live appender — host.log's handle
  lacks FILE_SHARE_DELETE, so a racing rename harmlessly fails).

Windows CI compile/clippy pending (drivers workspace + host are not
Linux-cross-checkable); rides along with the next pad-driver redeploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:03:32 +00:00
enricobuehlerandClaude Fable 5 82271d61dc feat(host/windows): "Punktfunk Host" identity in Task Manager (icon + version info)
punktfunk-host.exe embedded no icon or version resources, so Task Manager and
Explorer showed a bare lowercase exe name with a generic icon. build.rs now
embeds the branded .ico + FileDescription "Punktfunk Host" / ProductName
"Punktfunk" via winresource (same pattern as the Windows client and the tray;
Linux packaging builds skip the block). The tray gets a matching "Punktfunk
Tray" description, and the SCM display name moves off lowercase
"punktfunk streaming host" to "Punktfunk Host" (applied idempotently by
`service install` on upgrade).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 13:52:55 +00:00
enricobuehlerandClaude Fable 5 b292ce2c1f fix(packaging/windows): Windows 11 22H2 floor + tray install task + stale console-port fixes
The OS floor is now enforced at install time (MinVersion=10.0.22621 with an
explanatory [Messages] override): pf-vdisplay is built against IddCx 1.10, and
on Windows 10 (incl. LTSC) / Win11 21H2 the device fails start with Code 10
STATUS_DEVICE_POWER_FAILURE (field-reported). Docs (site requirements/install/
windows-host pages + README) state the floor; new docs-site Security page.

Installer also gains the trayicon task (punktfunk-tray.exe file + HKLM Run key,
post-install launch as the signed-in user, upgrade taskkill + uninstall
--quit/taskkill choreography before file deletion), and the wizard/cleanup
text/port sweeps move off the stale :3000 web-console references to :47992
(cleanups sweep both for upgrades from old installs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:09:52 +00:00
enricobuehlerandClaude Fable 5 ebada804b6 feat(tray): system-tray status icon for the host (Windows + Linux)
New crates/punktfunk-tray — a small per-user companion showing the host service
state at a glance (running / stopped / starting / degraded / failed + the live
session in the tooltip) with one-click actions: open web console, approve a
pending pairing request, start/stop/restart, open logs. No more digging through
logs to learn whether the service came back after a reboot or an update.

Status is service-manager-FIRST (SCM / systemd user unit — a port squatter can
never fake Running), then the new loopback-only unauthenticated
GET /api/v1/local/summary (counts/booleans only; the mgmt token and cert.pem
are SYSTEM/Admins-DACL'd on Windows, so a non-elevated tray cannot bearer-auth).

Windows: windows_subsystem binary (a console exe in the Run key would flash a
terminal at sign-in), Shell_NotifyIcon + hidden window, per-session single
instance, TaskbarCreated re-add, --quit for the uninstaller; service actions
elevate per click via ShellExecuteW "runas" onto the new
`punktfunk-host service restart` (stop → wait Stopped → start).
Linux: ksni/StatusNotifierItem over zbus, systemctl --user actions (no polkit),
/etc/xdg/autostart entry whose --autostart self-gates to actual host users.
Icons: scripts/gen-tray-icons.py (pure stdlib) renders the brand lens + status
dot into committed .ico/hicolor assets; deb/rpm/arch ship binary+autostart+icons.

Live-validated: Linux on the headless KDE session (SNI registration, state
transitions, menu-driven start, dbusmenu layout); Windows on the RTX box
(session-1 launch with no NIM_ADD failure, single instance, --quit, restart
round-trip, summary loopback-200/LAN-401).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:09:35 +00:00
enricobuehlerandClaude Fable 5 4482c97bd3 fix(encode/windows): resolve NVENC at runtime — AMD/Intel hosts no longer crash at start
The nvenc build linked nvEncodeAPI64.dll's entry points at load time, so a
--features nvenc binary hard-crashed on any box without the NVIDIA driver
(AMD/Intel). Entry points now come from a runtime LoadLibrary table
(encode/windows/nvenc.rs load_api); a missing DLL just falls through the
encoder auto-detect to AMF/QSV/software. The generated import lib and all its
plumbing (gen-nvenc-importlib.ps1, nvenc.def, PUNKTFUNK_NVENC_LIB_DIR,
setup-build-env wiring) are gone.

Live-validated on the RTX 4090 box (NVENC session, 7000+ frames).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:09:18 +00:00
enricobuehlerandClaude Fable 5 8b47be668f feat(host/windows): seal the host↔driver channels (frame + gamepad, proto v2)
Frame ring (pf-vdisplay) and both gamepad SHM channels move off named Global\
objects (openable by any sibling LocalService) to UNNAMED sections/events whose
handles the host DuplicateHandles into the driver's verified WUDFHost with least
access — frame delivery over the SYSTEM+admins-only IOCTL_SET_FRAME_CHANNEL,
pads over a 32-byte named bootstrap mailbox (pid + handle value only, DoS-bounded;
HID minidrivers have no control device). Driver-validated pad_index kills
cross-pad redirects; v1↔v2 mixes fail closed with diagnosis logs on both sides.
Sibling-LocalService denial proven empirically (design/idd-push-security.md,
design/gamepad-channel-sealing.md).

Driver-side raw ops now live behind pf-umdf-util (checked shm accessors, the
forbid(unsafe_code) ChannelClient state machine, WDF request tokens) — the pad
drivers' logic is 100% safe Rust; whole drivers workspace clippy-gated in CI.

driver install --gamepad now sweeps SWD\punktfunk phantom devnodes: a re-created
SwDevice REVIVES the old devnode with its previously-bound driver (never
re-ranks), so an upgrade otherwise leaves the old driver serving — or, across
the v1→v2 fence, a dead pad (found live on the RTX box).

On-glass validated on the RTX 4090 box: frame path 7007 frames p50 2.06 ms
cross-machine; DualSense + XUSB "sealed pad channel mapped"/proto=2 attach via
both the test harness and a real streaming session; phantom-sweep repro.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 12:08:56 +00:00
enricobuehlerandClaude Fable 5 af74c9ca67 feat(web,host/windows): move the web console off :3000 to :47992
windows-host / package (push) Has been cancelled
flatpak / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
web-screenshots / screenshots (push) Has been cancelled
rpm / build-publish (bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
release / apple (push) Has been cancelled
linux-client-screenshots / screenshots (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
decky / build-publish (push) Has been cancelled
deb / build-publish (push) Has been cancelled
android / android (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
android-screenshots / screenshots (push) Has been cancelled
Port 3000 collides with half the dev-server ecosystem; 47992 sits next
to the mgmt API (47990) in the punktfunk port family. Updates the run
scripts, systemd/scheduled-task units, Dockerfile, Windows firewall
rule + installer, packaging, and every doc that referenced :3000.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:17:42 +00:00
enricobuehlerandClaude Fable 5 6bbe7ae137 feat(host/windows,drivers): gamepad driver attach/heartbeat health surfaced in logs
The gamepad drivers have no IOCTL plane (hidclass gates the stack), so
until now the host had ZERO visibility into whether a driver ever
bound: a pad could be "created" with no driver installed and nothing
was logged. Two health fields are carved from reserved shm space
(layout-compatible; pf-driver-proto pins the offsets): driver_proto —
stamped by pf-xusb at device add + per serviced XInput IOCTL (movement
= the game-visible path) and by pf-dualsense/DS4 from its ~125Hz timer
— and driver_heartbeat. Host-side, every pad owns a DriverAttach
watcher fed from the existing service() poll: INFO on attach (WARN on
proto mismatch), and after 3s of silence ONE diagnosis WARN combining
a cached pnputil /enum-drivers store check, the devnode's CM problem
code (CM_Locate_DevNodeW/CM_Get_DevNode_Status on the instance id now
captured from the create callback, with plain-language hints: 28 = not
installed, 52 = signature/Memory Integrity, …) and the driver's debug
log path. Also fixes a real bug both SwDeviceCreate wrappers shared:
the 10s WaitForSingleObject result was ignored and the callback
HRESULT zero-initialised, so a PnP timeout read as SUCCESS (now E_FAIL
init + explicit timeout error). Failure-mode table:
design/gamepad-driver-health.md.

Linux workspace green; Windows host + drivers CI-compile only, on-box
recipe at the bottom of the design doc.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:33:56 +00:00
enricobuehlerandClaude Fable 5 6de4128298 feat(host/windows,packaging): installer overhaul - branding, VB-CABLE, GameStream choice, driver uninstall
- Modern branded wizard: WizardStyle=modern dynamic windows11 (Inno >= 6.6,
  plain-modern fallback for older compilers; CI provisioning upgrades a
  pre-6.6 Inno). Brand-mark wizard side panels + header tiles (100-200% DPI)
  and a multi-size punktfunk.ico (SetupIconFile + Apps & Features), generated
  AND committed by branding/gen-branding.ps1 from the canonical brand geometry.
  Gotcha encoded in the script: ISCC rejects all-PNG icons, so entries <= 64px
  are classic DIBs (PNG only at 128/256), and the ICO is load-verified.

- VB-CABLE actually ships now: windows-host.yml never set VBCABLE_DIR, so every
  published installer silently omitted the virtual mic (broken mic passthrough
  in the field). CI provisions the pinned, SHA-256-verified official Pack45
  (provision-windows-punktfunk-extras.ps1) and the pack now FAILS on a
  supplied-but-invalid dir instead of shipping mic-less again. Attribution per
  VB-Audio's bundling grant surfaced in the visible wizard task text (vendor,
  vb-cable.com, donationware) on top of the licenses notice.

- GameStream (Moonlight) compat is a wizard task (checked by default) ->
  service install --gamestream=on|off writes PUNKTFUNK_HOST_CMD=
  serve[ --gamestream] into host.env. Only the two canonical values are ever
  rewritten - a hand-customized command line survives upgrades. Silent
  installs: /MERGETASKS="!gamestream".

- Driver uninstall (field report: our virtual-device drivers survived
  uninstall): new `driver uninstall [--gamepad]` removes the pf-vdisplay
  device node(s) + the pf-vdisplay/pf-dualsense/pf-xusb driver-store packages,
  wired into [UninstallRun] after service uninstall. Locale-safe by
  construction: devices matched on unlocalized VALUES (never pnputil's
  localized labels), packages found by INF content scan - validated against a
  German-locale box ("Instanz-ID:" parse; 7/7 punktfunk INFs matched, no
  foreign hits). VB-CABLE is deliberately left installed (shared third-party
  component with its own uninstaller).

Installer compile, cargo check/clippy/fmt, and the ASCII locale gate are green;
the wizard look + uninstall flow still need one on-glass pass on a disposable
box (this box runs the live host).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 12:16:19 +02:00
enricobuehlerandClaude Opus 4.8 6e41356919 feat(protocol,clients): codec preference negotiation + Linux client decodes per Welcome (Phase 2a)
Adds a client-selectable **preferred codec** and wires the core + ABI + probe + Linux client to
negotiate and decode it. (Windows/Apple/Android follow in 2b.)

**Core:**
- `Hello.preferred_codec` (a single CODEC_* bit, 0 = auto) — a soft hint appended after
  `video_codecs`. `resolve_codec(client, host, preferred)` now honors the preference when the host
  can also emit it, else falls back to precedence (HEVC > AV1 > H.264). Roundtrip + preference tests.
- `NativeClient::connect` takes `video_codecs` + `preferred_codec`; `NativeClient.codec` exposes the
  resolved `Welcome.codec`.
- ABI: `punktfunk_connect_ex7` (adds the two codec params; `ex6` delegates to it advertising
  HEVC-only) + `punktfunk_connection_codec` getter + `PUNKTFUNK_CODEC_{H264,HEVC,AV1}` constants
  (drift-guarded against the wire values). Header regenerated.

**Host:** passes `hello.preferred_codec` into `resolve_codec`.

**probe:** `--codec h264|hevc|av1|auto` sets the preference (still advertises it can decode all
three); the dump extension already follows the resolved codec.

**Linux client:** advertises the codecs FFmpeg can actually decode (`decodable_codecs()`), threads
the user's `codec` setting as the preference, and builds the decoder — both the software and VAAPI
paths, plus the mid-session VAAPI→software demotion — from the negotiated `Welcome.codec` instead of
hardcoding HEVC. New "Video codec" dropdown in Preferences (Automatic/HEVC/H.264/AV1).

Live-validated on the dev box: probe `--codec hevc` against a software (H.264-only) host resolves to
H.264 (graceful soft-preference fallback), no failure. clippy + core (57) + host (133) tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 00:13:26 +00:00